Container Generation
IMPORTANT
[IocContainer] generates a compile-time container for specialized scenarios. It is not a replacement for MS.E.DI. For most applications, the generated Add{ProjectName} registration extension method (see Basic Usage) is all you need. Use [IocContainer] when you need:
- High-performance typed resolution with minimal overhead
- Tag-filtered containers — for example, a
MediatorContainerwithIncludeTags = ["Mediator"]for a specific subsystem - Strict compile-time resolution — ensuring all dependencies are verifiable at build time
[IocContainer] lets SourceGen.Ioc generate a compile-time container class.
Use this when you want typed, reflection-free service resolution without manually wiring every service.
Feature Coverage
[IocContainer] supports all registration patterns introduced in the earlier docs (02_Basic through 10_Wrapper): defaults, field/property/method injection, keyed services, decorators, tags, factory/instance registrations, open generics, wrappers, and imported modules.
Feature-gated behavior still follows SourceGenIocFeatures (for example, field injection requires FieldInject to be enabled), and host-specific integrations still require the corresponding interfaces and package references.
Basic Container
Start with the smallest setup:
public interface IClock
{
DateTimeOffset UtcNow { get; }
}
[IocRegister<IClock>(ServiceLifetime.Singleton)]
internal sealed class SystemClock : IClock
{
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
}
[IocContainer]
public partial class AppContainer;Generated Code
// <auto-generated/>
partial class AppContainer :
IIocContainer<global::AppContainer>,
IServiceProvider,
IKeyedServiceProvider,
IServiceProviderIsService,
IServiceProviderIsKeyedService,
ISupportRequiredService,
IServiceScopeFactory,
IServiceScope,
IDisposable,
IAsyncDisposable,
IServiceProviderFactory<IServiceCollection>
{
private readonly IServiceProvider? _fallbackProvider;
private global::SystemClock _systemClock = null!;
public AppContainer(IServiceProvider? fallbackProvider)
{
_fallbackProvider = fallbackProvider;
_systemClock = GetSystemClock();
}
private global::SystemClock GetSystemClock()
{
if (_systemClock is not null)
{
return _systemClock;
}
var instance = new global::SystemClock();
_systemClock = instance;
return instance;
}
public object? GetService(Type serviceType)
{
// local resolver lookup generated here
return _fallbackProvider?.GetService(serviceType);
}
}Generated Interfaces
The generated container can implement the following interfaces (depending on options and referenced packages):
| Interface | Purpose |
|---|---|
IIocContainer<TContainer> | SourceGen.Ioc typed container interface. |
IServiceProvider | Standard service resolution API. |
IKeyedServiceProvider | Keyed service resolution API. |
IServiceProviderIsService | Checks service availability. |
IServiceProviderIsKeyedService | Checks keyed service availability. |
ISupportRequiredService | Supports required-service semantics. |
IServiceScopeFactory | Creates child scopes. |
IServiceScope | Container instance is also a scope. |
IServiceProviderFactory<IServiceCollection> | Generated only when IntegrateServiceProvider = true and MS.E.DI package support is available; integrates the generated container with IServiceCollection (CreateBuilder / CreateServiceProvider). |
IDisposable / IAsyncDisposable | Disposes tracked services and scope resources. |
Container Options
Configure generation with IocContainerAttribute properties:
| Property | Default | Description |
|---|---|---|
IntegrateServiceProvider | true | Allow fallback to external IServiceProvider for unknown services. The generated container does not resolve IServiceCollection by itself. |
ExplicitOnly | false | Only include services explicitly associated with this container class. |
IncludeTags | [] | Include only services that match at least one tag. |
UseSwitchStatement | false | Use switch/if resolver path instead of FrozenDictionary (typically only beneficial for small service counts). |
ThreadSafeStrategy | ThreadSafeStrategy.Lock | Thread-safety model for singleton/scoped cache initialization. |
EagerResolveOptions | EagerResolveOptions.Singleton | Controls eager initialization for singleton/scoped services. |
NOTE
ExplicitOnly takes precedence over IncludeTags.
WARNING
If modules are imported with [IocImportModule], UseSwitchStatement = true is ignored and analyzer SGIOC020 is reported.
WARNING
Circular module imports (direct or transitive) are not allowed. The analyzer reports SGIOC025 as an error if a circular dependency is detected — for example, A → B → A or A → B → C → A.
IMPORTANT
The container generated by SourceGen.Ioc does not resolve IServiceCollection by itself. If you want to integrate with IServiceCollection, you must ensure that the generated container should be constructed with an external IServiceProvider.
TIP
For most .NET applications, the simplest way to integrate the generated container is to use it with MS.E.DI. Since the container implements IServiceProviderFactory<IServiceCollection>, you can call new AppContainer().CreateServiceProvider(services) to build a container that uses MS.E.DI as the fallback IServiceProvider.
Filtering by Tags
[IocRegister<IMessageBus>(Tags = ["Mediator"])]
internal sealed class MessageBus : IMessageBus;
[IocContainer(IncludeTags = ["Mediator"])]
public partial class MediatorContainer;Generated Code
// <auto-generated/>
private static readonly KeyValuePair<ServiceIdentifier, Func<global::MediatorContainer, object>>[] _localResolvers =
[
new(new ServiceIdentifier(typeof(IServiceProvider), global::Microsoft.Extensions.DependencyInjection.KeyedService.AnyKey), static c => c),
new(new ServiceIdentifier(typeof(IServiceScopeFactory), global::Microsoft.Extensions.DependencyInjection.KeyedService.AnyKey), static c => c),
new(new ServiceIdentifier(typeof(global::MediatorContainer), global::Microsoft.Extensions.DependencyInjection.KeyedService.AnyKey), static c => c),
new(new ServiceIdentifier(typeof(global::MessageBus), global::Microsoft.Extensions.DependencyInjection.KeyedService.AnyKey), static c => c.GetMessageBus()),
new(new ServiceIdentifier(typeof(global::IMessageBus), global::Microsoft.Extensions.DependencyInjection.KeyedService.AnyKey), static c => c.GetMessageBus()),
];NOTE
IncludeTags filters user registrations. Container/system entries (IServiceProvider, IServiceScopeFactory, and the container type itself) are still generated. Services imported via [IocImportModule] are also included as-is (not filtered by IncludeTags).
Thread Safety Strategy
ThreadSafeStrategy controls singleton/scoped cache initialization for lazy services:
| Strategy | Default | Behavior |
|---|---|---|
None | No | No synchronization. |
Lock | Yes | Double-checked locking (lock) strategy. |
SemaphoreSlim | No | Double-checked locking with SemaphoreSlim. |
SpinLock | No | Double-checked locking with SpinLock. |
CompareExchange | No | Lock-free compare-and-swap (Interlocked.CompareExchange). |
[IocContainer(ThreadSafeStrategy = ThreadSafeStrategy.CompareExchange)]
public partial class AppContainer;Generated Code
// <auto-generated/>
private global::MyService? _myService;
private global::MyService GetMyService()
{
if (_myService is not null)
{
return _myService;
}
var instance = new global::MyService();
var existing = Interlocked.CompareExchange(ref _myService, instance, null);
if (existing is not null)
{
DisposeService(instance);
return existing;
}
return instance;
}Eager Resolution
EagerResolveOptions determines whether singleton/scoped services are initialized in container/scope constructors.
| Option | Default | Description |
|---|---|---|
None | No | No eager initialization. |
Singleton | Yes | Eagerly initialize singleton services when root container is created. |
Scoped | No | Eagerly initialize scoped services when a child scope is created. |
SingletonAndScoped | No | Eagerly initialize both singleton and scoped services. |
[IocContainer(EagerResolveOptions = EagerResolveOptions.SingletonAndScoped)]
public partial class AppContainer;Generated Code
// <auto-generated/>
public AppContainer(IServiceProvider? fallbackProvider)
{
_fallbackProvider = fallbackProvider;
// eager singleton initialization
_testNamespace_SingletonService = GetTestNamespace_SingletonService();
}
private AppContainer(AppContainer parent)
{
_fallbackProvider = parent._fallbackProvider;
_isRootScope = false;
// copy singleton references from parent
_testNamespace_SingletonService = parent._testNamespace_SingletonService;
// eager scoped initialization
_testNamespace_ScopedService = GetTestNamespace_ScopedService();
}NOTE
_serviceResolvers is declared as a private static readonly field with a static initializer (_localResolvers.ToFrozenDictionary()), so it is not assigned in constructors or copied between scopes.
MVC and Blazor Activator Interfaces
The generated container can also provide host-specific activator implementations for ASP.NET Core MVC and Blazor. This happens when your container partial class explicitly declares the corresponding interface.
| Interface | Trigger | Generated Behavior |
|---|---|---|
IControllerActivator | Container declares IControllerActivator AND Microsoft.AspNetCore.Mvc.Core is referenced | Controller activation via ActivatorUtilities.CreateFactory, cached in ConcurrentDictionary |
IComponentActivator | Container declares IComponentActivator AND Microsoft.AspNetCore.Components is referenced | Component activation via ActivatorUtilities.CreateFactory, cached in ConcurrentDictionary; includes hot reload cache invalidation |
IComponentPropertyActivator | Container declares IComponentPropertyActivator AND Microsoft.AspNetCore.Components ≥ 11 is referenced | For registered components (with IComponentActivator): no-op delegate. For unregistered components: reflection-based [Inject]/[IocInject] property injection, cached in ConcurrentDictionary; includes hot reload cache invalidation |
IMPORTANT
SourceGen.Ioc's source generator only analyzes .cs files. For Blazor components, attributes like [IocContainer], [IocRegister], [IocInject] and interface declarations (for example, : IComponentActivator) must be placed in a code-behind file (.razor.cs or a separate .cs file), not in a .razor file. The source generator won't read .razor files, so attributes declared there will not trigger code generation.
MVC Example
[IocContainer]
public partial class AppContainer : IControllerActivator;Generated Code
// <auto-generated/>
partial class AppContainer : IControllerActivator
{
private static readonly ConcurrentDictionary<Type, ObjectFactory> _controllerFactoryCache = new();
object IControllerActivator.Create(ControllerContext context)
{
var controllerType = context.ActionDescriptor.ControllerTypeInfo.AsType();
var instance = GetService(controllerType);
if (instance is not null) return instance;
if (!_controllerFactoryCache.TryGetValue(controllerType, out var factory))
{
factory = CreateControllerFactory(controllerType);
_controllerFactoryCache.TryAdd(controllerType, factory);
}
return factory(this, []);
}
void IControllerActivator.Release(ControllerContext context, object controller)
{
if (controller is IDisposable disposable) disposable.Dispose();
}
}Blazor Example
// Must be in a .cs file (code-behind), not in a .razor file
[IocContainer]
public partial class ComponentContainer : IComponentActivator;Generated Code
// <auto-generated/>
[assembly: MetadataUpdateHandler(typeof(ComponentContainer.__HotReloadHandler))]
partial class ComponentContainer : IComponentActivator
{
private static readonly ConcurrentDictionary<Type, ObjectFactory> _componentFactoryCache = new();
IComponent IComponentActivator.CreateInstance(Type componentType)
{
var instance = GetService(componentType);
if (instance is IComponent component) return component;
if (!_componentFactoryCache.TryGetValue(componentType, out var factory))
{
factory = CreateComponentFactory(componentType);
_componentFactoryCache.TryAdd(componentType, factory);
}
return (IComponent)factory(this, []);
}
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class __HotReloadHandler
{
public static void ClearCache(Type[]? _)
{
_componentFactoryCache.Clear();
}
}
}Blazor Property Injection Example
NOTE
IComponentPropertyActivator is only available in ASP.NET Core 11 and later.
When your container implements IComponentPropertyActivator, the generator produces a GetActivator method that returns a delegate for injecting [Inject] properties into Blazor components at runtime.
If the container also implements IComponentActivator, registered components get a no-op delegate (property injection is already handled during IComponentActivator.CreateInstance). Unregistered components fall back to reflection-based property injection.
public interface IDataService { }
[IocRegister<IDataService>(ServiceLifetime.Singleton)]
public class DataService : IDataService { }
[IocRegister(ServiceLifetime.Transient)]
public class MyComponent : IComponent
{
[Inject]
public IDataService DataService { get; set; } = default!;
}
// Must be in a .cs file (code-behind), not in a .razor file
[IocContainer]
public partial class ComponentContainer : IComponentActivator, IComponentPropertyActivator;Generated Code (IComponentPropertyActivator region)
// <auto-generated/>
[assembly: MetadataUpdateHandler(typeof(ComponentContainer.__HotReloadHandler))]
partial class ComponentContainer : IComponentPropertyActivator
{
private static readonly ConcurrentDictionary<Type, Action<IServiceProvider, IComponent>> _propertyActivatorCache = new();
Action<IServiceProvider, IComponent> IComponentPropertyActivator.GetActivator(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType)
{
if (!_propertyActivatorCache.TryGetValue(componentType, out var activator))
{
activator = _serviceResolvers.ContainsKey(
new ServiceIdentifier(componentType, KeyedService.AnyKey))
? static (_, _) => { }
: CreateComponentPropertyInjector(componentType);
_propertyActivatorCache.TryAdd(componentType, activator);
}
return activator;
}
private static Action<IServiceProvider, IComponent> CreateComponentPropertyInjector(
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType)
{
const BindingFlags flags = BindingFlags.Instance
| BindingFlags.Public | BindingFlags.NonPublic;
List<(PropertyInfo Property, object? Key)>? injectables = null;
for (var type = componentType; type is not null; type = type.BaseType)
{
foreach (var property in type.GetProperties(flags))
{
if (property.DeclaringType != type) continue;
var injectAttr = property.GetCustomAttributes(true)
.FirstOrDefault(a =>
a.GetType().Name is "InjectAttribute" or "IocInjectAttribute");
if (injectAttr is null) continue;
var keyProp = injectAttr.GetType().GetProperty("Key");
var key = keyProp?.GetValue(injectAttr);
injectables ??= new();
injectables.Add((property, key));
}
}
if (injectables is null) return static (_, _) => { };
return (serviceProvider, component) =>
{
foreach (var (property, serviceKey) in injectables)
{
object? value;
if (serviceKey is not null)
{
var keyedProvider = serviceProvider as IKeyedServiceProvider
?? throw new InvalidOperationException(
$"Cannot provide a value for property '{property.Name}' " +
$"on type '{componentType.FullName}'. " +
$"The service provider does not implement 'IKeyedServiceProvider'.");
value = keyedProvider.GetRequiredKeyedService(
property.PropertyType, serviceKey);
}
else
{
value = serviceProvider.GetService(property.PropertyType)
?? throw new InvalidOperationException(
$"Cannot provide a value for property '{property.Name}' " +
$"on type '{componentType.FullName}'. " +
$"There is no registered service of type '{property.PropertyType}'.");
}
property.SetValue(component, value);
}
};
}
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class __HotReloadHandler
{
public static void ClearCache(Type[]? _)
{
_componentFactoryCache.Clear();
_propertyActivatorCache.Clear();
}
}
}TIP
When implementing IComponentPropertyActivator without IComponentActivator, the no-op optimization does not apply — all components use the reflection-based fallback because the container cannot guarantee that constructor injection went through CreateInstance.
Hot Reload Support
When IComponentActivator or IComponentPropertyActivator is implemented, the generator also emits:
- An
[assembly: MetadataUpdateHandler]attribute pointing to an internal nested__HotReloadHandlerclass - A
__HotReloadHandlerclass with aClearCache(Type[]?)method that clears the activator caches on hot reload
This ensures that cached ObjectFactory instances and property activator delegates are invalidated when types change during development, matching the behavior of ASP.NET Core's built-in DefaultComponentActivator and DefaultComponentPropertyActivator.
NOTE
The hot reload handler is only generated when at least one of IComponentActivator or IComponentPropertyActivator is implemented. ClearCache only clears the caches that actually exist based on which interfaces are implemented.
Partial Accessors (Fast-Path Service Resolution)
Declare partial methods or partial properties in the [IocContainer] class to get strongly-typed, fast-path accessors for registered services. The generator implements them automatically — no dictionary lookup, no boxing.
Supported Patterns
| Pattern | Declaration | Generated Implementation |
|---|---|---|
| Partial method | public partial IMyService GetMyService(); | Calls the internal resolver directly |
| Partial property | public partial IMyService MyService { get; } | Calls the internal resolver directly |
| Nullable (optional) | public partial IMyService? TryGetMyService(); | Returns null if unresolvable |
| Keyed service | [IocInject("key")] public partial IMyService GetMyService(); | Resolves with the specified key |
Basic Partial Method
public interface IMyService { }
[IocRegister<IMyService>(ServiceLifetime.Singleton)]
public class MyService : IMyService { }
[IocContainer]
public partial class AppContainer
{
public partial IMyService GetMyService();
}Generated Code
// <auto-generated/>
partial class AppContainer
{
// Internal resolver
private global::TestNamespace.MyService _testNamespace_MyService = null!;
private global::TestNamespace.MyService GetTestNamespace_MyService()
{
if(_testNamespace_MyService is not null) return _testNamespace_MyService;
var instance = new global::TestNamespace.MyService();
_testNamespace_MyService = instance;
return instance;
}
// Partial accessor — direct call, no dictionary lookup
public partial global::TestNamespace.IMyService GetMyService() => GetTestNamespace_MyService();
}Partial Property
[IocContainer]
public partial class AppContainer
{
public partial IMyService MyService { get; }
}Generated Code
// <auto-generated/>
partial class AppContainer
{
public partial global::TestNamespace.IMyService MyService { get => GetTestNamespace_MyService(); }
}Nullable (Optional) Accessor
When the return type is nullable, the accessor falls back to the container's GetService and returns null if the service is not registered — it never throws.
[IocContainer]
public partial class AppContainer
{
public partial IMyService? TryGetMyService();
}Generated Code
// <auto-generated/>
partial class AppContainer
{
public partial global::TestNamespace.IMyService? TryGetMyService()
=> GetService(typeof(global::TestNamespace.IMyService)) as global::TestNamespace.IMyService;
}Keyed Service Accessor
Add [IocInject("key")] to resolve a keyed registration:
public interface ICache { }
[IocRegister<ICache>(ServiceLifetime.Singleton, Key = "redis")]
public class RedisCache : ICache { }
[IocContainer]
public partial class AppContainer
{
[IocInject("redis")]
public partial ICache GetRedisCache();
}Generated Code
// <auto-generated/>
partial class AppContainer
{
public partial global::TestNamespace.ICache GetRedisCache() => GetTestNamespace_RedisCache__redis_();
}Resolution Rules
- The return type is matched against registered service types (fully qualified name).
- If
[IocInject]is present, itsKeyis used for keyed service resolution. - If a matching internal resolver exists, it is called directly (fast-path).
- If no internal resolver exists but
IntegrateServiceProvider = true, fallback toGetService/GetRequiredService. - If no resolver exists and
IntegrateServiceProvider = false:- Nullable return type → returns
default - Non-nullable return type →
throw new InvalidOperationException(…)(and reportsSGIOC021)
- Nullable return type → returns
Naming Conflict Handling
If a user-declared partial method name collides with an internally generated resolver name (e.g., both named GetMyService), the generator automatically renames the internal resolver with a _Resolve suffix:
// User declares:
public partial IMyService GetMyService();
// Generator renames internal resolver to avoid conflict:
// GetMyService → GetMyService_ResolveGenerated Code
// <auto-generated/>
partial class TestContainer
{
// Internal resolver — renamed to avoid conflict
private global::MyService _myService = null!;
private global::MyService GetMyService_Resolve()
{
if(_myService is not null) return _myService;
var instance = new global::MyService();
_myService = instance;
return instance;
}
// Partial accessor calls the renamed resolver
public partial global::IMyService GetMyService() => GetMyService_Resolve();
}Diagnostics
| ID | Severity | Description |
|---|---|---|
| SGIOC018 | Error | Container cannot resolve a dependency when IntegrateServiceProvider = false. |
| SGIOC019 | Error | Container class must be partial and cannot be static. |
| SGIOC020 | Warning | UseSwitchStatement = true is ignored when importing modules. |
| SGIOC021 | Error | Non-nullable partial accessor return type is not a registered service when IntegrateServiceProvider = false. |
| SGIOC025 | Error | Container has a circular module import dependency (direct or transitive). |