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
Task<T>Async-init wrapper — resolves Task<T> directly or wraps sync service via Task.FromResult

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.

Task<T>

Task<T> is the async-init wrapper. When a consumer depends on Task<T>, the generator routes resolution based on whether the inner service T uses async method injection or not.

NOTE

When the inner service uses async method injection (requires the AsyncMethodInject feature flag), the generator emits a Task<T> registration automatically. For sync-only services, the consumer’s Task<T> dependency is wrapped with Task.FromResult. See MSBuild Configuration for how to enable AsyncMethodInject.

Async-init Service

When the inner service T has [IocInject] on an async Task method, the generator emits a Task<TImplementation> registration plus a forwarding Task<TService> registration. The consumer resolves Task<T> directly:

csharp
using System.Threading.Tasks;

public interface IMyService;
public interface IDependency;

[IocRegister<IMyService>(ServiceLifetime.Singleton)]
internal class MyService : IMyService
{
    [IocInject]
    public async Task InitAsync(IDependency dep)
    {
        await Task.CompletedTask;
    }
}

[IocRegister]
internal class Consumer(Task<IMyService> serviceTask)
{
    public async Task<IMyService> GetServiceAsync() => await serviceTask;
}
Generated Code
csharp
// <auto-generated/>
services.AddSingleton<global::MyNamespace.Dependency, global::MyNamespace.Dependency>();
services.AddSingleton<global::System.Threading.Tasks.Task<global::MyNamespace.MyService>>((global::System.IServiceProvider sp) =>
{
    async global::System.Threading.Tasks.Task<global::MyNamespace.MyService> Init()
    {
        var s0_m0 = sp.GetRequiredService<global::MyNamespace.IDependency>();
        var s0 = new global::MyNamespace.MyService();
        await s0.InitAsync(s0_m0);
        return s0;
    }
    return Init();
});
// Forwarding registration: Task<IMyService> → Task<MyService>
services.AddSingleton<global::System.Threading.Tasks.Task<global::MyNamespace.IMyService>>(async (global::System.IServiceProvider sp) => await sp.GetRequiredService<global::System.Threading.Tasks.Task<global::MyNamespace.MyService>>());
services.AddSingleton<global::MyNamespace.Consumer>((global::System.IServiceProvider sp) =>
{
    var p0 = sp.GetRequiredService<global::System.Threading.Tasks.Task<global::MyNamespace.IMyService>>();
    var s0 = new global::MyNamespace.Consumer(p0);
    return s0;
});

NOTE

The generator follows a fixed injection stage order: properties → fields → synchronous methods → async methods. See Async Method Injection for details.

Sync-only Service

When the inner service T does not have async inject methods, the generator wraps the synchronous resolution with Task.FromResult:

csharp
public interface ISyncService;

[IocRegister<ISyncService>]
internal class SyncService : ISyncService;

[IocRegister]
internal class Consumer(Task<ISyncService> serviceTask)
{
    public async Task<ISyncService> GetServiceAsync() => await serviceTask;
}
Generated Code
csharp
// <auto-generated/>
services.AddSingleton<global::MyNamespace.SyncService, global::MyNamespace.SyncService>();
services.AddSingleton<global::MyNamespace.ISyncService>((global::System.IServiceProvider sp) => sp.GetRequiredService<global::MyNamespace.SyncService>());

// Consumer — sync-only service wrapped with Task.FromResult
services.AddSingleton<global::MyNamespace.Consumer>((global::System.IServiceProvider sp) =>
{
    var p0 = global::System.Threading.Tasks.Task.FromResult(sp.GetRequiredService<global::MyNamespace.ISyncService>());
    var s0 = new global::MyNamespace.Consumer(p0);
    return s0;
});

Unsupported Nesting

Nested Task<T> wrappers are not supported. The following patterns will not be recognized:

PatternStatus
Task<Lazy<T>>Not supported
Lazy<Task<T>>Not supported
IEnumerable<Task<T>>Not supported

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));

Nesting Limits

Wrapper nesting behavior is shape-dependent:

  • Non-collection outer wrappers (Lazy<T>, Func<T>): recursively resolved to arbitrary depth via inline construction.
  • Collection outer wrappers (IEnumerable<T>, IList<T>, etc.): support at most 1 level of inner wrapping (2 levels total). Deeper nesting falls back to default behavior.

Why Inline Construction?

Inline construction for nested wrappers is a deliberate pragmatic choice.

  • Directly resolving nested wrappers from the container (for example, container.GetService<Lazy<Func<T>>>()) is extremely rare; the primary use case is constructor injection, which inline construction fully covers.
  • Keeping nested wrappers inline avoids extending field scanning, naming conventions, and scoped container infrastructure for every nested wrapper shape.
  • Nested wrappers typically do not require cross-consumer instance sharing, so each consumer holding its own inline-constructed instance is semantically correct.

Supported

PatternBehavior
Lazy<Func<T>>Inline factory: new Lazy<Func<T>>(() => new Func<T>(...))
Func<Lazy<T>>Inline factory: new Func<Lazy<T>>(() => new Lazy<T>(...))
Lazy<IEnumerable<T>>Inline: new Lazy<IEnumerable<T>>(() => sp.GetServices<T>())
Lazy<Func<Lazy<T>>>Recursively inline (non-collection outer wrapper)
IEnumerable<Lazy<T>>Resolved via standalone Lazy<T> registrations
IEnumerable<Func<T>>Resolved via standalone Func<T> registrations

Not supported (collection outer wrapper with 3+ levels)

PatternBehavior
IEnumerable<Lazy<Func<T>>>No wrapper registrations emitted
IEnumerable<Func<Lazy<T>>>No wrapper registrations emitted

When a collection outer wrapper contains 3+ levels of nesting:

  • Register pipeline: The consumer is registered with a plain AddXXX<Consumer, Consumer>() call. No wrapper registrations are emitted, so the nested wrapper parameter depends on MS.DI runtime resolution.
  • Container pipeline: The parameter falls back to IServiceProvider resolution via GetRequiredService(typeof(...)).

NOTE

ValueTask<T> is not a recognized wrapper type in any context. Only Task<T> is supported for async-init wrapping. When used as a partial accessor return type: if the target service uses async-init, diagnostic SGIOC029 is reported; otherwise diagnostic SGIOC021 (unresolvable type) is reported.

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.