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