Skip to content

Wrapper Types

SourceGen.Ioc recognizes several wrapper types and generates the appropriate resolution code automatically.
When a registered service depends on a wrapper type, the generator emits factory delegates or collection lookups at compile time — no runtime reflection required.

Supported Wrapper Types

WrapperResolution
Lazy<T>new Lazy<T>(() => sp.GetRequiredService<T>())
Func<T>() => sp.GetRequiredService<T>()
Func<T1, ..., TReturn>Parameterized factory delegate
IEnumerable<T>MS.E.DI native collection support
IReadOnlyCollection<T>GetServices<T>().ToArray()
ICollection<T>GetServices<T>().ToArray()
IReadOnlyList<T>GetServices<T>().ToArray()
IList<T>GetServices<T>().ToArray()
T[]GetServices<T>().ToArray()
IDictionary<TKey, TValue>Dictionary built from keyed service entries
KeyValuePair<TKey, TValue>Single keyed service entry

Lazy<T>

Inject Lazy<T> to defer service creation until first access:

csharp
public interface IExpensiveService;

[IocRegister<IExpensiveService>]
internal class ExpensiveService : IExpensiveService;

[IocRegister]
internal class Consumer(Lazy<IExpensiveService> lazyService)
{
    public IExpensiveService Service => lazyService.Value;
}
Generated Code
csharp
// <auto-generated/>
services.AddSingleton<global::MyNamespace.ExpensiveService, global::MyNamespace.ExpensiveService>();
services.AddSingleton<global::MyNamespace.IExpensiveService>((global::System.IServiceProvider sp) => sp.GetRequiredService<global::MyNamespace.ExpensiveService>());
services.AddSingleton<global::MyNamespace.Consumer, global::MyNamespace.Consumer>();

// Lazy wrapper registrations
services.AddSingleton<global::System.Lazy<global::MyNamespace.IExpensiveService>>((global::System.IServiceProvider sp) =>
    new global::System.Lazy<global::MyNamespace.IExpensiveService>(
        () => sp.GetRequiredService<global::MyNamespace.ExpensiveService>(),
        global::System.Threading.LazyThreadSafetyMode.ExecutionAndPublication));

Func<T>

Inject Func<T> to create a new service instance on each invocation:

csharp
public interface IWorker;

[IocRegister<IWorker>(ServiceLifetime.Transient)]
internal class Worker : IWorker;

[IocRegister]
internal class JobRunner(Func<IWorker> workerFactory)
{
    public IWorker CreateWorker() => workerFactory();
}
Generated Code
csharp
// <auto-generated/>
services.AddTransient<global::MyNamespace.Worker, global::MyNamespace.Worker>();
services.AddTransient<global::MyNamespace.IWorker>((global::System.IServiceProvider sp) => sp.GetRequiredService<global::MyNamespace.Worker>());
services.AddSingleton<global::MyNamespace.JobRunner, global::MyNamespace.JobRunner>();

// Func wrapper registrations
services.AddTransient<global::System.Func<global::MyNamespace.IWorker>>((global::System.IServiceProvider sp) =>
    new global::System.Func<global::MyNamespace.IWorker>(() => sp.GetRequiredService<global::MyNamespace.Worker>()));

Parameterized Func<T1, ..., TReturn>

Use Func<T1, ..., TReturn> when the service constructor requires parameters that should be provided at call time rather than resolved from DI. The generator matches Func input types to constructor parameters by type, and resolves remaining parameters from the container.

csharp
public interface IService;
public interface ILogger;

[IocRegister<ILogger>(ServiceLifetime.Scoped)]
internal class Logger : ILogger;

[IocRegister<IService>(ServiceLifetime.Scoped)]
internal class MyService(string name, ILogger logger) : IService;

[IocRegister]
internal class Consumer(Func<string, IService> serviceFactory)
{
    public IService Create(string name) => serviceFactory(name);
}
Generated Code
csharp
// <auto-generated/>
services.AddScoped<global::MyNamespace.Logger, global::MyNamespace.Logger>();
services.AddScoped<global::MyNamespace.ILogger>((global::System.IServiceProvider sp) => sp.GetRequiredService<global::MyNamespace.Logger>());
services.AddScoped<global::MyNamespace.MyService, global::MyNamespace.MyService>();
services.AddScoped<global::MyNamespace.IService>((global::System.IServiceProvider sp) => sp.GetRequiredService<global::MyNamespace.MyService>());
services.AddSingleton<global::MyNamespace.Consumer, global::MyNamespace.Consumer>();

// Func wrapper registrations
services.AddScoped<global::System.Func<string, global::MyNamespace.IService>>((global::System.IServiceProvider sp) =>
    new global::System.Func<string, global::MyNamespace.IService>((string arg0) =>
    {
        var p0 = sp.GetRequiredService<global::MyNamespace.ILogger>();
        var s0 = new global::MyNamespace.MyService(arg0, p0);
        return s0;
    }));

NOTE

The generator matches Func input types (string in this example) to the implementation constructor parameters by type. Unmatched constructor parameters (ILogger) are resolved from the service provider automatically.

Collection Wrappers

Inject collection types to receive all registrations of a service type:

csharp
public interface IPlugin;

[IocRegister<IPlugin>]
internal class PluginA : IPlugin;

[IocRegister<IPlugin>]
internal class PluginB : IPlugin;

[IocRegister]
internal class PluginHost(IReadOnlyList<IPlugin> plugins);
Generated Code
csharp
// <auto-generated/>
services.AddSingleton<global::MyNamespace.PluginA, global::MyNamespace.PluginA>();
services.AddSingleton<global::MyNamespace.IPlugin>((global::System.IServiceProvider sp) => sp.GetRequiredService<global::MyNamespace.PluginA>());
services.AddSingleton<global::MyNamespace.PluginB, global::MyNamespace.PluginB>();
services.AddSingleton<global::MyNamespace.IPlugin>((global::System.IServiceProvider sp) => sp.GetRequiredService<global::MyNamespace.PluginB>());
services.AddSingleton<global::MyNamespace.PluginHost, global::MyNamespace.PluginHost>();

NOTE

IEnumerable<T> is resolved natively by MS.E.DI. Other collection types (IReadOnlyList<T>, IList<T>, T[], etc.) are resolved via GetServices<T>().ToArray().

IDictionary<TKey, TValue>

For keyed services, inject IDictionary<TKey, TValue> to get all keyed registrations as a dictionary:

csharp
public interface ICache;

[IocRegister<ICache>(Key = "memory")]
internal class MemoryCache : ICache;

[IocRegister<ICache>(Key = "redis")]
internal class RedisCache : ICache;

[IocRegister]
internal class CacheConsumer(IDictionary<string, ICache> caches);
Generated Code
csharp
// <auto-generated/>
services.AddSingleton<global::MyNamespace.CacheConsumer>((global::System.IServiceProvider sp) =>
{
    var p0 = sp.GetServices<global::System.Collections.Generic.KeyValuePair<string, global::MyNamespace.ICache>>()
        .ToDictionary();
    var s0 = new global::MyNamespace.CacheConsumer(p0);
    return s0;
});

// KeyValuePair registrations for keyed services
services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(
    typeof(global::System.Collections.Generic.KeyValuePair<string, global::MyNamespace.ICache>),
    (global::System.IServiceProvider sp) => (object)new global::System.Collections.Generic.KeyValuePair<string, global::MyNamespace.ICache>(
        "memory", sp.GetRequiredKeyedService<global::MyNamespace.ICache>("memory")),
    global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton));
services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(
    typeof(global::System.Collections.Generic.KeyValuePair<string, global::MyNamespace.ICache>),
    (global::System.IServiceProvider sp) => (object)new global::System.Collections.Generic.KeyValuePair<string, global::MyNamespace.ICache>(
        "redis", sp.GetRequiredKeyedService<global::MyNamespace.ICache>("redis")),
    global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton));

TIP

The generator automatically emits KeyValuePair registrations for each keyed service matching the dictionary's value type. IDictionary<TKey, TValue> is then built by calling GetServices<KeyValuePair<TKey, TValue>>().ToDictionary().

KeyValuePair<TKey, TValue>

You can also inject KeyValuePair<TKey, TValue> directly to receive a single keyed service entry:

csharp
public interface IHandler;

[IocRegister<IHandler>(Key = "handler1")]
internal class Handler1 : IHandler;

[IocRegister<IHandler>(Key = "handler2")]
internal class Handler2 : IHandler;

[IocRegister]
internal class Consumer(KeyValuePair<string, IHandler> entry);
Generated Code
csharp
// <auto-generated/>
services.AddSingleton<global::MyNamespace.Consumer>((global::System.IServiceProvider sp) =>
{
    var p0 = new global::System.Collections.Generic.KeyValuePair<string, global::MyNamespace.IHandler>(
        default, sp.GetRequiredService<global::MyNamespace.IHandler>());
    var s0 = new global::MyNamespace.Consumer(p0);
    return s0;
});

// KeyValuePair registrations for keyed services
services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(
    typeof(global::System.Collections.Generic.KeyValuePair<string, global::MyNamespace.IHandler>),
    (global::System.IServiceProvider sp) => (object)new global::System.Collections.Generic.KeyValuePair<string, global::MyNamespace.IHandler>(
        "handler1", sp.GetRequiredKeyedService<global::MyNamespace.IHandler>("handler1")),
    global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton));
services.Add(new global::Microsoft.Extensions.DependencyInjection.ServiceDescriptor(
    typeof(global::System.Collections.Generic.KeyValuePair<string, global::MyNamespace.IHandler>),
    (global::System.IServiceProvider sp) => (object)new global::System.Collections.Generic.KeyValuePair<string, global::MyNamespace.IHandler>(
        "handler2", sp.GetRequiredKeyedService<global::MyNamespace.IHandler>("handler2")),
    global::Microsoft.Extensions.DependencyInjection.ServiceLifetime.Singleton));

NOTE

KeyValuePair<TKey, TValue> is a struct, so the generator uses ServiceDescriptor directly with a boxing factory instead of the generic AddSingleton<T> overload (which has a class constraint). When injecting a single KeyValuePair, the resolved entry depends on GetServices ordering — prefer IDictionary<TKey, TValue> when you need all keyed entries.

Wrapper Nesting

Wrapper types can be nested. For example, IEnumerable<Lazy<IMyService>> is valid:

csharp
public interface IHandler;

[IocRegister<IHandler>]
internal class HandlerA : IHandler;

[IocRegister<IHandler>]
internal class HandlerB : IHandler;

[IocRegister]
internal class Orchestrator(IEnumerable<Lazy<IHandler>> lazyHandlers);
Generated Code
csharp
// <auto-generated/>
services.AddSingleton<global::MyNamespace.HandlerA, global::MyNamespace.HandlerA>();
services.AddSingleton<global::MyNamespace.IHandler>((global::System.IServiceProvider sp) => sp.GetRequiredService<global::MyNamespace.HandlerA>());
services.AddSingleton<global::MyNamespace.HandlerB, global::MyNamespace.HandlerB>();
services.AddSingleton<global::MyNamespace.IHandler>((global::System.IServiceProvider sp) => sp.GetRequiredService<global::MyNamespace.HandlerB>());
services.AddSingleton<global::MyNamespace.Orchestrator, global::MyNamespace.Orchestrator>();

// Lazy wrapper registrations
services.AddSingleton<global::System.Lazy<global::MyNamespace.IHandler>>((global::System.IServiceProvider sp) =>
    new global::System.Lazy<global::MyNamespace.IHandler>(
        () => sp.GetRequiredService<global::MyNamespace.HandlerA>(),
        global::System.Threading.LazyThreadSafetyMode.ExecutionAndPublication));
services.AddSingleton<global::System.Lazy<global::MyNamespace.IHandler>>((global::System.IServiceProvider sp) =>
    new global::System.Lazy<global::MyNamespace.IHandler>(
        () => sp.GetRequiredService<global::MyNamespace.HandlerB>(),
        global::System.Threading.LazyThreadSafetyMode.ExecutionAndPublication));

With Open Generics

Wrapper dependencies can trigger closed generic discovery for open generic registrations.

Lazy<T> and Func<T>

csharp
public interface IRequestHandler<TRequest, TResponse>;

[IocRegister(ServiceTypes = [typeof(IRequestHandler<,>)])]
internal class Handler<TRequest, TResponse> : IRequestHandler<TRequest, TResponse>;

public sealed record Ping;

[IocRegister]
internal class Consumer(
    Lazy<IRequestHandler<Ping, string>> lazyHandler,
    Func<IRequestHandler<Ping, string>> handlerFactory);
Generated Code
csharp
// <auto-generated/>
services.AddSingleton(typeof(global::MyNamespace.Handler<,>), typeof(global::MyNamespace.Handler<,>));

// Closed generic discovered from wrapper dependencies
services.AddSingleton<global::MyNamespace.Handler<global::MyNamespace.Ping, string>, global::MyNamespace.Handler<global::MyNamespace.Ping, string>>();
services.AddSingleton<global::MyNamespace.IRequestHandler<global::MyNamespace.Ping, string>>((global::System.IServiceProvider sp) => sp.GetRequiredService<global::MyNamespace.Handler<global::MyNamespace.Ping, string>>());
services.AddSingleton<global::MyNamespace.Consumer, global::MyNamespace.Consumer>();

// Lazy wrapper registrations
services.AddSingleton<global::System.Lazy<global::MyNamespace.IRequestHandler<global::MyNamespace.Ping, string>>>((global::System.IServiceProvider sp) =>
    new global::System.Lazy<global::MyNamespace.IRequestHandler<global::MyNamespace.Ping, string>>(
        () => sp.GetRequiredService<global::MyNamespace.Handler<global::MyNamespace.Ping, string>>(),
        global::System.Threading.LazyThreadSafetyMode.ExecutionAndPublication));

// Func wrapper registrations
services.AddSingleton<global::System.Func<global::MyNamespace.IRequestHandler<global::MyNamespace.Ping, string>>>((global::System.IServiceProvider sp) =>
    new global::System.Func<global::MyNamespace.IRequestHandler<global::MyNamespace.Ping, string>>(
        () => sp.GetRequiredService<global::MyNamespace.Handler<global::MyNamespace.Ping, string>>()));

IDictionary<TKey, TValue>

csharp
public interface ICache;

[IocRegister<ICache>(Key = "memory")]
internal class MemoryCache : ICache;

[IocRegister<ICache>(Key = "redis")]
internal class RedisCache : ICache;

[IocRegister]
internal class CacheConsumer(IDictionary<string, ICache> caches);
Generated Code
csharp
// <auto-generated/>
services.AddSingleton<global::MyNamespace.CacheConsumer>((global::System.IServiceProvider sp) =>
{
    var p0 = sp.GetServices<global::System.Collections.Generic.KeyValuePair<string, global::MyNamespace.ICache>>()
        .ToDictionary();
    var s0 = new global::MyNamespace.CacheConsumer(p0);
    return s0;
});

← Back to Overview

Released under the MIT License.