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
{
private readonly IServiceProvider? _fallbackProvider;
private global::SystemClock _systemClock = null!;
public AppContainer() : this((IServiceProvider?)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 GetFallbackServiceProvider()?.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> | Implemented by the nested AppContainer.ServiceProviderFactory type, not by the container. 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: scoped services (a scope's own, or those resolved directly from the root), then singletons in the root. Transient services are not tracked or disposed; dispose them yourself. Synchronous Dispose throws InvalidOperationException when a tracked service only implements IAsyncDisposable — use DisposeAsync in that case. |
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 registered by [IocRegisterFor] on this container class. Each one exposes the same service types as an assembly registration (ServiceTypes, RegisterAllInterfaces, RegisterAllBaseClasses, and [IocRegisterDefaults]). Open generic ones are closed for the container's dependencies, GetService<T>() calls, and [IocDiscover] on the 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.
NOTE
A container can import other containers, including containers from other assemblies. See Importing Modules Across Assemblies.
IMPORTANT
The container generated by SourceGen.Ioc does not resolve IServiceCollection by itself. To integrate with IServiceCollection, construct the generated container with an external IServiceProvider (for example, the one built by MS.E.DI).
TIP
For most .NET applications, the simplest way to integrate the generated container is to use it with MS.E.DI. The generated nested type AppContainer.ServiceProviderFactory implements IServiceProviderFactory<IServiceCollection>, so you can call new AppContainer.ServiceProviderFactory().CreateServiceProvider(services) (or pass it to UseServiceProviderFactory) to build a container that uses MS.E.DI as the fallback IServiceProvider. The factory is not a container, so no singleton is created until the returned container is built. The returned container owns that MS.E.DI provider and disposes it with the container. A provider you pass to the constructor yourself is not disposed by the container. Each scope created from the container opens a matching scope on the fallback provider the first time it resolves a service from it, so scoped MS.E.DI services (for example, a DbContext) are scoped per container scope. Scopes that never use the fallback provider do not create one.
Filtering by Tags
[IocRegister<IMessageBus>(Tags = ["Mediator"])]
internal sealed class MessageBus : IMessageBus;
[IocContainer(IncludeTags = ["Mediator"])]
public partial class MediatorContainer;Generated Code
// <auto-generated/>
using __IocResolver = global::System.Collections.Generic.KeyValuePair<global::SourceGen.Ioc.ServiceIdentifier, global::System.Func<global::MediatorContainer, object>>;
private static readonly KeyValuePair<ServiceIdentifier, Func<global::MediatorContainer, object>>[] _localResolvers =
[
__Resolver(typeof(IServiceProvider), static c => c),
__Resolver(typeof(IServiceScopeFactory), static c => c),
__Resolver(typeof(global::MediatorContainer), static c => c),
__Resolver(typeof(global::MessageBus), static c => c.GetMessageBus()),
__Resolver(typeof(global::IMessageBus), 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).
Importing Modules Across Assemblies
A container can import another [IocContainer] class — a module — with [IocImportModule]. The module is typically defined in a shared library, so each assembly generates its own container and the application container combines them.
// Shared library (IocSample.Shared)
[IocContainer]
public sealed partial class SharedModule;
// Application
[IocImportModule<SharedModule>]
// Or non-generic version
// [IocImportModule(typeof(SharedModule))]
[IocContainer]
public sealed partial class Module;Generated Code
// <auto-generated/>
partial class Module : IIocContainer<global::IocSample.Module>, IServiceProvider /* ... */
{
// Each container instance owns its own module instance
private readonly global::IocSample.Shared.SharedModule _iocSample_Shared_SharedModule;
public Module(IServiceProvider? fallbackProvider)
{
_fallbackProvider = fallbackProvider;
_iocSample_Shared_SharedModule = new global::IocSample.Shared.SharedModule(fallbackProvider);
}
private Module(Module parent, IIocFallbackScopeOwner? fallbackScopeOwner)
{
// A scope of this container creates a scope of the module, which shares this scope's fallback scope
_iocSample_Shared_SharedModule = (global::IocSample.Shared.SharedModule)((IIocFallbackScopeOwner)parent._iocSample_Shared_SharedModule).CreateScope(this).ServiceProvider;
}
// Imported resolvers first, local resolvers last
private static readonly FrozenDictionary<ServiceIdentifier, Func<global::IocSample.Module, object>> _serviceResolvers =
global::IocSample.Shared.SharedModule.Resolvers.Select(static kvp => new KeyValuePair<ServiceIdentifier, Func<global::IocSample.Module, object>>(kvp.Key, c => kvp.Value(c._iocSample_Shared_SharedModule)))
.Concat(_localResolvers)
.ToFrozenDictionary();
public void Dispose()
{
// ... dispose local services
_iocSample_Shared_SharedModule.Dispose();
}
}How It Works
- Resolvers — Every generated container exposes its resolvers through the static
IIocContainer<TContainer>.Resolversproperty. The importing container combines the resolvers of each module with its own into a singleFrozenDictionary, and a resolver from a module runs against the module instance. - Lifetime — Each container instance creates its own module instance, so module singletons are shared within that root container and the scopes created from it, not across separate root containers. Creating a scope also creates a scope of each module, so module scoped services follow the importing container's scopes.
- Disposal — Disposing the container (or one of its scopes) also disposes the corresponding module instance (or module scope).
- Dependencies — A module resolves its services' dependencies from its own registrations and then from the fallback
IServiceProviderpassed to the importing container. It cannot see services registered only in the importing container. - Fallback scope — Within one scope of the importing container, a scoped service of the fallback
IServiceProvider(e.g. aDbContext) is the same instance whether a module service or a container service receives it. A module built with an earlier SourceGen.Ioc version opens its own fallback scope instead, until it is rebuilt. - Fallback provider — A module with
IntegrateServiceProvider = falseis created without the fallbackIServiceProvider. - Defaults —
[IocRegisterDefaults]declared in the module's assembly also apply to the importing assembly. See Default Settings. - Open generics — Open generic registrations of the module (and of modules it imports) are available for closed generic discovery in the importing container.
Duplicate Registrations
When the same service type and key is registered in more than one place, the last one wins (ToFrozenDictionary keeps the last duplicate):
- Modules in the order of their
[IocImportModule]attributes — a later module overrides an earlier one. - Local registrations — always override imported ones.
Limitations
WARNING
If modules are imported, UseSwitchStatement = true is ignored and analyzer SGIOC020 is reported, because services from multiple sources are combined at runtime.
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. Module resolvers are built in static initializers, so a cycle would deadlock type initialization.
NOTE
IncludeTags and ExplicitOnly do not filter imported services; the module's own options decide what it contains.
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). |
NOTE
Services with async inject methods use Lock when the container is configured with SpinLock or CompareExchange, so their initialization never runs twice. Choose SemaphoreSlim if concurrent first resolutions should wait asynchronously instead of blocking a thread.
[IocContainer(ThreadSafeStrategy = ThreadSafeStrategy.CompareExchange)]
public partial class AppContainer;Generated Code
// <auto-generated/>
private global::MyService? _myService;
private global::MyService GetMyService() => _myService ?? CreateMyService();
private global::MyService CreateMyService()
{
var instance = new global::MyService();
var existing = global::System.Threading.Interlocked.CompareExchange(ref _myService, instance, null);
if (existing is not null)
{
((object)instance as global::System.IDisposable)?.Dispose();
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, IIocFallbackScopeOwner? fallbackScopeOwner)
{
_fallbackProvider = parent._fallbackProvider;
_fallbackScopeOwner = fallbackScopeOwner;
_isRootScope = false;
// copy singleton references from parent
_testNamespace_SingletonService = parent._testNamespace_SingletonService;
// eager scoped initialization
_testNamespace_ScopedService = GetTestNamespace_ScopedService();
}NOTE
Lazy/Func wrapper fields are initialized before eager services, so eager services can safely inject Lazy<T> / Func<T>. A singleton that is not eagerly created is always created by the root container, even when it is first requested from a scope.
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 |
| Wrapper or collection | public partial Lazy<IMyService> LazyService { get; }, public partial IMyService[] GetAll(); | Resolved like a constructor parameter of that type |
| Service of an imported module | public partial IModuleService GetModuleService(); | Looks the service up in the container, which includes its imported modules |
NOTE
Accessors must be instance members. Partial methods must be parameterless, non-generic, and return a value; partial properties need a getter. Other partial members are not implemented by the generator.
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_();
}Async-init Service Accessor
When the implementation has async inject methods (see Async Method Injection), or a decorator applied to the service has them (see Async Decorators), the service can only be resolved asynchronously. Declare the accessor with a Task<T> return type:
[IocContainer]
public partial class AppContainer
{
public partial Task<IMyService> GetMyServiceAsync();
}Returning IMyService directly reports SGIOC027. Any other shape, such as ValueTask<IMyService> or Lazy<Task<IMyService>>, reports SGIOC029.
The same rule applies to runtime resolution: an async-init service is registered as Task<IMyService>, matching the IServiceCollection registration. Resolve it with GetRequiredService<Task<IMyService>>(); GetService<IMyService>() returns null.
Wrapper, Collection and Module Accessors
An accessor's return type is resolved exactly like a constructor parameter of the same type, so wrappers, collections and services of imported modules work too:
[IocContainer(ExplicitOnly = true)]
[IocRegisterFor<ModuleService>(ServiceLifetime.Singleton, ServiceTypes = [typeof(IModuleService)])]
public partial class ServiceModule;
[IocContainer(IntegrateServiceProvider = false, ExplicitOnly = true)]
[IocImportModule<ServiceModule>]
[IocRegisterFor<MyService>(ServiceLifetime.Singleton, ServiceTypes = [typeof(IMyService)])]
public partial class AppContainer
{
public partial IModuleService GetModuleService();
public partial Task<IModuleService> GetModuleServiceAsync();
public partial Lazy<IMyService> LazyService { get; }
public partial IMyService[] Services { get; }
}Generated Code
// <auto-generated/>
partial class AppContainer
{
// Not registered by this container: looked up in the container's resolvers, which include its imported modules
public partial global::TestNamespace.IModuleService GetModuleService()
=> (global::TestNamespace.IModuleService)GetRequiredService(typeof(global::TestNamespace.IModuleService));
// The module's Task<T> for an async-init service, otherwise a completed task
public partial global::System.Threading.Tasks.Task<global::TestNamespace.IModuleService> GetModuleServiceAsync()
=> (GetService(typeof(global::System.Threading.Tasks.Task<global::TestNamespace.IModuleService>)) as global::System.Threading.Tasks.Task<global::TestNamespace.IModuleService>
?? global::System.Threading.Tasks.Task.FromResult((global::TestNamespace.IModuleService)GetRequiredService(typeof(global::TestNamespace.IModuleService))));
// The container's shared Lazy<IMyService>
public partial global::System.Lazy<global::TestNamespace.IMyService> LazyService { get => _lazy_IMyService_myService; }
// A new array per call
public partial global::TestNamespace.IMyService[] Services { get => GetAllIMyServiceArray(); }
}Resolution Rules
- If
[IocInject]is present, itsKeyis used for keyed service resolution. - If the container registers the service, its internal resolver is called directly (fast-path).
- Wrappers (
Lazy<T>,Func<T>, …) and collections are resolved like constructor parameters of that type. - Any other service is looked up with
GetService(nullable accessor) orGetRequiredService(non-nullable accessor). This finds services of imported modules and, whenIntegrateServiceProvider = true, the fallback provider. - If nothing registers the service:
- Nullable return type → returns
null - Non-nullable return type → throws
InvalidOperationExceptionwhen called; withIntegrateServiceProvider = false,SGIOC021reports it at compile time
- 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 or a record. |
| SGIOC020 | Warning | UseSwitchStatement = true is ignored when importing modules. |
| SGIOC021 | Error | Non-nullable partial accessor return type is registered neither in the container nor in its imported modules when IntegrateServiceProvider = false. |
| SGIOC025 | Error | Container has a circular module import dependency (direct or transitive). |
| SGIOC027 | Error | Partial accessor returns T directly, but the service is async-init (its implementation or a decorator has async inject methods). Return Task<T> instead. |
| SGIOC029 | Error | Partial accessor targets an async-init service with a return type other than Task<T> (for example ValueTask<T> or Lazy<Task<T>>). |
| SGIOC031 | Warning | Container is not generated because Container is not enabled in SourceGenIocFeatures. |