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
| Wrapper | Resolution |
|---|---|
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:
public interface IExpensiveService;
[IocRegister<IExpensiveService>]
internal class ExpensiveService : IExpensiveService;
[IocRegister]
internal class Consumer(Lazy<IExpensiveService> lazyService)
{
public IExpensiveService Service => lazyService.Value;
}Generated Code
// <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:
public interface IWorker;
[IocRegister<IWorker>(ServiceLifetime.Transient)]
internal class Worker : IWorker;
[IocRegister]
internal class JobRunner(Func<IWorker> workerFactory)
{
public IWorker CreateWorker() => workerFactory();
}Generated Code
// <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.
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
// <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:
public interface IPlugin;
[IocRegister<IPlugin>]
internal class PluginA : IPlugin;
[IocRegister<IPlugin>]
internal class PluginB : IPlugin;
[IocRegister]
internal class PluginHost(IReadOnlyList<IPlugin> plugins);Generated Code
// <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:
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
// <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:
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
// <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:
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
// <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:
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
// <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:
| Pattern | Status |
|---|---|
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:
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
// <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
| Pattern | Behavior |
|---|---|
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)
| Pattern | Behavior |
|---|---|
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
IServiceProviderresolution viaGetRequiredService(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>
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
// <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>
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
// <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;
});