Skip to content

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 MediatorContainer with IncludeTags = ["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:

csharp
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
csharp
// <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):

InterfacePurpose
IIocContainer<TContainer>SourceGen.Ioc typed container interface.
IServiceProviderStandard service resolution API.
IKeyedServiceProviderKeyed service resolution API.
IServiceProviderIsServiceChecks service availability.
IServiceProviderIsKeyedServiceChecks keyed service availability.
ISupportRequiredServiceSupports required-service semantics.
IServiceScopeFactoryCreates child scopes.
IServiceScopeContainer 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 / IAsyncDisposableDisposes tracked services and scope resources.

Container Options

Configure generation with IocContainerAttribute properties:

PropertyDefaultDescription
IntegrateServiceProvidertrueAllow fallback to external IServiceProvider for unknown services. The generated container does not resolve IServiceCollection by itself.
ExplicitOnlyfalseOnly include services explicitly associated with this container class.
IncludeTags[]Include only services that match at least one tag.
UseSwitchStatementfalseUse switch/if resolver path instead of FrozenDictionary (typically only beneficial for small service counts).
ThreadSafeStrategyThreadSafeStrategy.LockThread-safety model for singleton/scoped cache initialization.
EagerResolveOptionsEagerResolveOptions.SingletonControls 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

csharp
[IocRegister<IMessageBus>(Tags = ["Mediator"])]
internal sealed class MessageBus : IMessageBus;

[IocContainer(IncludeTags = ["Mediator"])]
public partial class MediatorContainer;
Generated Code
csharp
// <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:

StrategyDefaultBehavior
NoneNoNo synchronization.
LockYesDouble-checked locking (lock) strategy.
SemaphoreSlimNoDouble-checked locking with SemaphoreSlim.
SpinLockNoDouble-checked locking with SpinLock.
CompareExchangeNoLock-free compare-and-swap (Interlocked.CompareExchange).
csharp
[IocContainer(ThreadSafeStrategy = ThreadSafeStrategy.CompareExchange)]
public partial class AppContainer;
Generated Code
csharp
// <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.

OptionDefaultDescription
NoneNoNo eager initialization.
SingletonYesEagerly initialize singleton services when root container is created.
ScopedNoEagerly initialize scoped services when a child scope is created.
SingletonAndScopedNoEagerly initialize both singleton and scoped services.
csharp
[IocContainer(EagerResolveOptions = EagerResolveOptions.SingletonAndScoped)]
public partial class AppContainer;
Generated Code
csharp
// <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.

InterfaceTriggerGenerated Behavior
IControllerActivatorContainer declares IControllerActivator AND Microsoft.AspNetCore.Mvc.Core is referencedController activation via ActivatorUtilities.CreateFactory, cached in ConcurrentDictionary
IComponentActivatorContainer declares IComponentActivator AND Microsoft.AspNetCore.Components is referencedComponent activation via ActivatorUtilities.CreateFactory, cached in ConcurrentDictionary; includes hot reload cache invalidation
IComponentPropertyActivatorContainer declares IComponentPropertyActivator AND Microsoft.AspNetCore.Components ≥ 11 is referencedFor 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

csharp
[IocContainer]
public partial class AppContainer : IControllerActivator;
Generated Code
csharp
// <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

csharp
// Must be in a .cs file (code-behind), not in a .razor file
[IocContainer]
public partial class ComponentContainer : IComponentActivator;
Generated Code
csharp
// <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.

csharp
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)
csharp
// <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 __HotReloadHandler class
  • A __HotReloadHandler class with a ClearCache(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

PatternDeclarationGenerated Implementation
Partial methodpublic partial IMyService GetMyService();Calls the internal resolver directly
Partial propertypublic 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

csharp
public interface IMyService { }

[IocRegister<IMyService>(ServiceLifetime.Singleton)]
public class MyService : IMyService { }

[IocContainer]
public partial class AppContainer
{
    public partial IMyService GetMyService();
}
Generated Code
csharp
// <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

csharp
[IocContainer]
public partial class AppContainer
{
    public partial IMyService MyService { get; }
}
Generated Code
csharp
// <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.

csharp
[IocContainer]
public partial class AppContainer
{
    public partial IMyService? TryGetMyService();
}
Generated Code
csharp
// <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:

csharp
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
csharp
// <auto-generated/>
partial class AppContainer
{
    public partial global::TestNamespace.ICache GetRedisCache() => GetTestNamespace_RedisCache__redis_();
}

Resolution Rules

  1. The return type is matched against registered service types (fully qualified name).
  2. If [IocInject] is present, its Key is used for keyed service resolution.
  3. If a matching internal resolver exists, it is called directly (fast-path).
  4. If no internal resolver exists but IntegrateServiceProvider = true, fallback to GetService / GetRequiredService.
  5. If no resolver exists and IntegrateServiceProvider = false:
    • Nullable return type → returns default
    • Non-nullable return type → throw new InvalidOperationException(…) (and reports SGIOC021)

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:

csharp
// User declares:
public partial IMyService GetMyService();

// Generator renames internal resolver to avoid conflict:
// GetMyService → GetMyService_Resolve
Generated Code
csharp
// <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

IDSeverityDescription
SGIOC018ErrorContainer cannot resolve a dependency when IntegrateServiceProvider = false.
SGIOC019ErrorContainer class must be partial and cannot be static.
SGIOC020WarningUseSwitchStatement = true is ignored when importing modules.
SGIOC021ErrorNon-nullable partial accessor return type is not a registered service when IntegrateServiceProvider = false.
SGIOC025ErrorContainer has a circular module import dependency (direct or transitive).

← Back to Overview

Released under the MIT License.