Best Practices
This guide provides production-oriented recommendations for SourceGen.Ioc. Use it as a companion to the feature documents in this folder.
Relationship to MS.E.DI
Although SourceGen.Ioc is built on top of MS.E.DI.A, it does not resolve IServiceCollection, think of it as an extension to MS.E.DI, not a replacement.
It generates IServiceCollection registration code at compile time, use these generated methods alongside your existing MS.E.DI setup to improve compile-time safety and reduce boilerplate.
The optional [IocContainer] is a specialized compile-time container for high-performance or tag-filtered scenarios. For general-purpose app composition, keep MS.E.DI as the primary container and use SourceGen.Ioc's generated registration methods as an extension layer.
For service registration fundamentals, see Basic Usage. For container-specific behavior and options, see Container.
Golden Path
Use this baseline in most projects:
- Keep domain code attribute-free — Use
[IocRegisterFor]and[IocRegisterDefaults]in dedicated registration files so business types never referenceSourceGen.Ioc. - Use
[IocRegister]only on infrastructure types you own where local annotation improves readability. - Prefer constructor injection by default.
- Use tags for startup-time profile selection, and keys for resolve-time selection.
- Add
[IocContainer]only when you need high-performance typed resolution or a tag-filtered container for a specific subsystem.
TIP
Use assembly-level or marker/container class placement when using [IocRegisterFor] and [IocRegisterDefaults] in dedicated registration files. This keeps all DI configuration in one place and your business types completely attribute-free.
// === Registration file (DI configuration is centralized here) ===
// Non-intrusive: register external or domain types without modifying them
[assembly: IocRegisterFor<SystemClock>(ServiceLifetime.Singleton, ServiceTypes = [typeof(IClock)])]
// Shared policy: all IHandler implementations are Scoped
[assembly: IocRegisterDefaults<IHandler>(ServiceLifetime.Scoped)]
// === Domain types stay attribute-free ===
public interface IClock
{
DateTimeOffset UtcNow { get; }
}
// No [IocRegister] needed — registered via [IocRegisterFor] above
internal sealed class SystemClock : IClock
{
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
}
// Infrastructure type — local annotation is fine here
[IocRegister]
internal sealed class MyHandler : IHandler;Generated Code
services.AddSingleton<global::MyNamespace.SystemClock, global::MyNamespace.SystemClock>();
services.AddSingleton<global::MyNamespace.IClock>((global::System.IServiceProvider sp) =>
sp.GetRequiredService<global::MyNamespace.SystemClock>());
services.AddScoped<global::MyNamespace.MyHandler, global::MyNamespace.MyHandler>();
services.AddScoped<global::MyNamespace.IHandler>((global::System.IServiceProvider sp) =>
sp.GetRequiredService<global::MyNamespace.MyHandler>());Registration Decision Matrix
Choose the registration mechanism by intent:
| Scenario | Use | Reason |
|---|---|---|
| Keep domain types attribute-free | [IocRegisterFor] / [IocRegisterFor<T>] | Registers any type without modifying its source code. |
| Many implementations share policy | [IocRegisterDefaults] | Centralized lifetime/key/tag/decorator policy. With ImplementationTypes, no per-class attributes needed. |
| Reuse defaults from another module | [IocImportModule] | Shares policy across modules and assemblies. |
| Infrastructure type you own | [IocRegister] / [IocRegister<T>] | Most explicit — local annotation improves readability for infra code. |
TIP
Start with [IocRegisterFor] and [IocRegisterDefaults] to keep your codebase non-intrusive. Use [IocRegister] only for types where local annotation adds clarity.
Lifetime Design Rules
Treat lifetime diagnostics as architecture feedback:
| Rule | Diagnostic | Recommended Action |
|---|---|---|
| Avoid circular dependencies | SGIOC002 | Break the cycle using abstractions, Func<T>, Lazy<T>, or boundary refactoring. |
| Singleton should not depend on scoped | SGIOC003 | Promote dependency lifetime or move access behind a singleton-safe abstraction. |
| Singleton should not depend on transient | SGIOC004 | Stabilize lifetime or redesign composition. |
| Scoped should not depend on transient | SGIOC005 | Promote transient dependency or split responsibilities. |
NOTE
Better fix lifetime diagnostics in design, not by suppressing analyzers.
Keys vs Tags
Use keys and tags for different phases:
| Need | Prefer | Why |
|---|---|---|
| Select implementation at resolve time | Keyed services | Natural one-to-one selection model. |
| Select profile at registration time | Tags | Natural startup profile model. |
| Tenant, region, or payment strategy selection | Keyed services | Easy lookup with GetRequiredKeyedService. |
| Feature bundles or app mode switches | Tags | Switches registration groups in one call. |
NOTE
Tag behavior is mutually exclusive: no-tag services are registered only when no tags are passed.
Generated Code
public static IServiceCollection AddMyProject(this IServiceCollection services, params IEnumerable<string> tags)
{
if (!tags.Any())
{
// register no-tag services
}
if (tags.Contains("Feature1"))
{
// register Feature1 services
}
return services;
}Defaults and Override Strategy
Prefer this order:
- Keep defaults on service contracts (
interface/ base type). - Override at implementation only when behavior truly differs.
- Keep decorator and key policy close to default declarations.
Settings and registration behavior follow documented precedence. See Default Settings for full details.
Decorator Strategy
- Keep decorator chains short and intentional.
- Place cross-cutting concerns (logging, metrics, tracing) in defaults.
- Use per-implementation override only for exceptional behavior.
- Validate the generated order in code review.
NOTE
Decorators are applied in declared order (first declared decorator is outermost in behavior).
Generated Code
services.AddScoped<global::MyNamespace.MyHandler, global::MyNamespace.MyHandler>();
services.AddScoped<global::MyNamespace.IHandler>((global::System.IServiceProvider sp) =>
{
var s0 = sp.GetRequiredService<global::MyNamespace.MyHandler>();
var s1 = new global::MyNamespace.MetricsDecorator<global::MyNamespace.MyHandler>(s0);
var s2 = new global::MyNamespace.LoggingDecorator<global::MyNamespace.MetricsDecorator<global::MyNamespace.MyHandler>>(s1);
return s2;
});Open Generic Strategy
- Start with regular open-generic registration.
- Use auto-discovery for common dependency graph closure.
- Add
[IocDiscover]only when you need explicit discover control. - Re-check generated output when combining nested generics with decorators.
Generated Code
services.AddTransient(typeof(global::MyNamespace.IHandler<>), typeof(global::MyNamespace.Handler<>));Injection Style Priority
Use this order for maintainability:
- Constructor injection
- Property injection
- Method injection
- Field injection
WARNING
If SourceGenIocFeatures disables a member injection feature, [IocInject] on that member is ignored and SGIOC022 is reported.
Container Recommendations
IMPORTANT
[IocContainer] is a specialized compile-time container API — it is not a full replacement for MS.E.DI. The generated container does not parse IServiceCollection registrations, so common extension-method registrations (for example services.AddLogging(), services.AddOptions(), services.AddHttpClient()) are not available in container-only mode. Use this container when you need high performance or a container tailored for specific subsystems (for example, a tag-filtered MediatorContainer).
NOTE
For general-purpose app composition, keep MS.E.DI as the primary container and use SourceGen.Ioc-generated registration methods as an extension layer. When IntegrateServiceProvider = true, unresolved services can still fall back to an external IServiceProvider.
Default recommendations for most apps:
IntegrateServiceProvider = trueExplicitOnly = falseUseSwitchStatement = falseThreadSafeStrategy = ThreadSafeStrategy.LockEagerResolveOptions = EagerResolveOptions.Singleton
Only tighten options with clear intent:
- Use
IntegrateServiceProvider = falsefor strict compile-time-only resolution boundaries. - Use
ExplicitOnly = truefor tightly controlled container surfaces. - Use
UseSwitchStatement = trueonly for small service sets and no imported modules.
WARNING
With imported modules, UseSwitchStatement = true is ignored and SGIOC020 is reported.
Generated Code
// <auto-generated/>
partial class AppContainer :
IIocContainer<global::AppContainer>,
IServiceProvider,
IServiceScopeFactory,
IServiceScope,
IDisposable,
IAsyncDisposable
{
private readonly IServiceProvider? _fallbackProvider;
public AppContainer(IServiceProvider? fallbackProvider)
{
_fallbackProvider = fallbackProvider;
}
// generated resolvers and cached service fields
}CLI Workflow for Existing Codebases
Use a safe CLI sequence:
- Start with
dry-run. - Restrict scope with regex and target path.
- Review diff.
- Apply changes.
- Build and resolve diagnostics.
sourcegen-ioc add -t ./src -s -cn ".*Service" -n -v
sourcegen-ioc add -t ./src -s -cn ".*Service"
sourcegen-ioc generate ioc-defaults -o ./Generated/Defaults.g.cs -t ./src -s -cn ".*" -b "I.*"Viewing Generated Code
Add the following MSBuild properties to output the generated source files into your project tree, making them available for code review, debugging, and source control:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>This writes all source generator output to the Generated/ folder. You can check these files into version control to track changes across generator updates.
TIP
If you only want the files for local inspection (not in source control), add Generated/ to your .gitignore instead.
Diagnostics Quick Fix Map
| Diagnostic | Meaning | First Place to Check |
|---|---|---|
SGIOC001 | Invalid registration target (private / abstract) | Basic Usage |
SGIOC002 | Circular dependency | Overview |
SGIOC003 | Singleton depends on scoped | Overview |
SGIOC004 | Singleton depends on transient | Overview |
SGIOC005 | Scoped depends on transient | Overview |
SGIOC006 | [FromKeyedServices] + [IocInject] conflict | Keyed Services |
SGIOC007 | Invalid [IocInject] usage | Injection |
SGIOC008 | Invalid/inaccessible Factory or Instance member | Factory & Instance |
SGIOC009 | Instance requires singleton lifetime | Factory & Instance |
SGIOC010 | Both Factory and Instance are specified | Factory & Instance |
SGIOC011 | Duplicate registration | Basic Usage |
SGIOC012 | Duplicate defaults | Default Settings |
SGIOC013 | [ServiceKey] type mismatch | Keyed Services |
SGIOC014 | [ServiceKey] used without keyed registration | Keyed Services |
SGIOC015 | Key type mismatch in keyed wrappers | Keyed Services |
SGIOC016 | Generic factory missing [IocGenericFactory] | Factory & Instance |
SGIOC017 | Duplicate placeholder types in [IocGenericFactory] | Factory & Instance |
SGIOC018 | Container dependency unresolved with strict integration | Container |
SGIOC019 | Invalid container type declaration | Container |
SGIOC020 | UseSwitchStatement ignored with imported modules | Container |
SGIOC021 | Container accessor return type unresolved with strict integration | Container |
SGIOC022 | Injection feature disabled by MSBuild feature flags | Injection, MSBuild Configuration |
SGIOC023 | Unrecognized InjectMembers element format | Injection |
SGIOC024 | Non-injectable member specified via InjectMembers | Injection |
Production Checklist
- Treat
SGIOC002toSGIOC005as release-blocking design issues. - Run CLI commands in
dry-runmode before bulk edits. - Use conservative container defaults unless benchmark data supports changes.
- Keep this checklist and your project-level conventions close to your app docs.