Automatically register services in the dependency injection container using attributes instead of manual registration code. The generator creates type-safe registration code at compile time, eliminating boilerplate and reducing errors.
Key Benefits:
- π― Zero boilerplate - Attribute-based registration eliminates manual
AddScoped<T>()calls - π Compile-time safety - Catch registration errors at build time, not runtime
- β‘ Auto-detection - Automatically registers all implemented interfaces
- π§ Multi-project support - Smart naming for assembly-specific registration methods
- π¨ Advanced patterns - Generics, keyed services, factories, decorators, and more
Quick Example:
// Input: Attribute decoration
[Registration(Lifetime.Scoped)]
public class UserService : IUserService { }
// Generated output
services.AddScoped<IUserService, UserService>();- π Feature Roadmap - See all implemented and planned features
- π― Sample Projects - Working code examples with architecture diagrams
- π― Dependency Registration Generator
- οΏ½ Documentation Navigation
- π Table of Contents
- π Get Started - Quick Guide
- π Project Structure
- 1οΈβ£ Setup Projects
- 2οΈβ£ Data Access Layer (PetStore.DataAccess) πΎ
- 3οΈβ£ Domain Layer (PetStore.Domain) π§
- 4οΈβ£ API Layer (PetStore.Api) π
- 5οΈβ£ Program.cs (Minimal API Setup) β‘
- π¨ What Gets Generated
- 6οΈβ£ Testing the Application π§ͺ
- π Viewing Generated Code (Optional)
- π― Key Takeaways
- π¦ Installation
- π‘ Basic Usage
- β¨ Features
- ποΈ Multi-Project Setup
- π Auto-Detection
- β±οΈ Service Lifetimes
- π Native AOT Compatibility
- βοΈ RegistrationAttribute Parameters
- π‘οΈ Diagnostics
- π· Generic Interface Registration
- π Keyed Service Registration
- π Factory Method Registration
- π¦ Instance Registration
- Basic Instance Registration (Static Field)
- Instance Registration with Static Property
- Instance Registration with Static Method
- Instance Registration Requirements
- Instance Registration with Multiple Interfaces
- Instance Registration Best Practices
- Instance Registration Diagnostics
- Instance vs Factory Method
- π TryAdd* Registration
- π« Assembly Scanning Filters
- π― Runtime Filtering
- π¨ Decorator Pattern
- ποΈ Conditional Registration
- π Additional Examples
This guide demonstrates a realistic 3-layer architecture for a PetStore application using minimal APIs.
PetStore.sln
βββ PetStore.Api/ (Presentation layer)
βββ PetStore.Domain/ (Business logic layer)
βββ PetStore.DataAccess/ (Data access layer)
PetStore.DataAccess.csproj (Base layer):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Atc.SourceGenerators" Version="1.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
</ItemGroup>
</Project>PetStore.Domain.csproj (Middle layer):
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Atc.SourceGenerators" Version="1.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.0" />
<ProjectReference Include="..\PetStore.DataAccess\PetStore.DataAccess.csproj" />
</ItemGroup>
</Project>PetStore.Api.csproj (Top layer):
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Atc.SourceGenerators" Version="1.0.0" />
<ProjectReference Include="..\PetStore.Domain\PetStore.Domain.csproj" />
</ItemGroup>
</Project>Note: The
Microsoft.Extensions.DependencyInjection.Abstractionspackage is needed for projects that use theIServiceCollectionextension methods. Logging requiresMicrosoft.Extensions.Logging.Abstractions.
Models/Pet.cs:
namespace PetStore.DataAccess.Models;
public class Pet
{
public int Id { get; set; }
public required string Name { get; set; }
public required string Species { get; set; }
public int Age { get; set; }
}Repositories/IPetRepository.cs:
namespace PetStore.DataAccess.Repositories;
public interface IPetRepository
{
Task<Pet?> GetByIdAsync(int id);
Task<IEnumerable<Pet>> GetAllAsync();
Task<int> CreateAsync(Pet pet);
Task<bool> UpdateAsync(Pet pet);
Task<bool> DeleteAsync(int id);
}Repositories/PetRepository.cs:
using Atc.DependencyInjection;
using PetStore.DataAccess.Models;
namespace PetStore.DataAccess.Repositories;
[Registration(Lifetime.Scoped)]
public class PetRepository : IPetRepository
{
// In-memory storage for demo purposes
private static readonly List<Pet> InMemoryPets = new()
{
new Pet { Id = 1, Name = "Buddy", Species = "Dog", Age = 3 },
new Pet { Id = 2, Name = "Whiskers", Species = "Cat", Age = 5 },
new Pet { Id = 3, Name = "Goldie", Species = "Fish", Age = 1 }
};
private static int _nextId = 4;
public Task<Pet?> GetByIdAsync(int id)
{
var pet = InMemoryPets.FirstOrDefault(p => p.Id == id);
return Task.FromResult(pet);
}
public Task<IEnumerable<Pet>> GetAllAsync()
{
return Task.FromResult(InMemoryPets.AsEnumerable());
}
public Task<int> CreateAsync(Pet pet)
{
pet.Id = _nextId++;
InMemoryPets.Add(pet);
return Task.FromResult(pet.Id);
}
public Task<bool> UpdateAsync(Pet pet)
{
var existing = InMemoryPets.FirstOrDefault(p => p.Id == pet.Id);
if (existing is null) return Task.FromResult(false);
existing.Name = pet.Name;
existing.Species = pet.Species;
existing.Age = pet.Age;
return Task.FromResult(true);
}
public Task<bool> DeleteAsync(int id)
{
var pet = InMemoryPets.FirstOrDefault(p => p.Id == id);
if (pet is null) return Task.FromResult(false);
InMemoryPets.Remove(pet);
return Task.FromResult(true);
}
}Models/PetDto.cs:
namespace PetStore.Domain.Models;
public record PetDto(int Id, string Name, string Species, int Age);
public record CreatePetRequest(string Name, string Species, int Age);
public record UpdatePetRequest(string Name, string Species, int Age);
public record ValidationResult(bool IsValid, List<string> Errors);Services/IPetService.cs:
namespace PetStore.Domain.Services;
public interface IPetService
{
Task<PetDto?> GetPetAsync(int id);
Task<IEnumerable<PetDto>> GetAllPetsAsync();
Task<int> CreatePetAsync(CreatePetRequest request);
Task<bool> UpdatePetAsync(int id, UpdatePetRequest request);
Task<bool> DeletePetAsync(int id);
}Services/PetService.cs:
using Atc.DependencyInjection;
using Microsoft.Extensions.Logging;
using PetStore.DataAccess.Models;
using PetStore.DataAccess.Repositories;
using PetStore.Domain.Models;
namespace PetStore.Domain.Services;
[Registration(Lifetime.Scoped)]
public class PetService : IPetService
{
private readonly IPetRepository _repository;
private readonly ILogger<PetService> _logger;
public PetService(IPetRepository repository, ILogger<PetService> logger)
{
_repository = repository;
_logger = logger;
}
public async Task<PetDto?> GetPetAsync(int id)
{
_logger.LogInformation("Getting pet with ID {PetId}", id);
var pet = await _repository.GetByIdAsync(id);
return pet is not null ? MapToDto(pet) : null;
}
public async Task<IEnumerable<PetDto>> GetAllPetsAsync()
{
_logger.LogInformation("Getting all pets");
var pets = await _repository.GetAllAsync();
return pets.Select(MapToDto);
}
public async Task<int> CreatePetAsync(CreatePetRequest request)
{
_logger.LogInformation("Creating pet: {PetName}", request.Name);
var pet = new Pet
{
Name = request.Name,
Species = request.Species,
Age = request.Age
};
return await _repository.CreateAsync(pet);
}
public async Task<bool> UpdatePetAsync(int id, UpdatePetRequest request)
{
_logger.LogInformation("Updating pet with ID {PetId}", id);
var pet = new Pet
{
Id = id,
Name = request.Name,
Species = request.Species,
Age = request.Age
};
return await _repository.UpdateAsync(pet);
}
public async Task<bool> DeletePetAsync(int id)
{
_logger.LogInformation("Deleting pet with ID {PetId}", id);
return await _repository.DeleteAsync(id);
}
private static PetDto MapToDto(Pet pet) => new(pet.Id, pet.Name, pet.Species, pet.Age);
}Validators/IPetValidator.cs:
using PetStore.Domain.Models;
namespace PetStore.Domain.Validators;
public interface IPetValidator
{
ValidationResult Validate(CreatePetRequest request);
}Validators/PetValidator.cs:
using Atc.DependencyInjection;
using PetStore.Domain.Models;
namespace PetStore.Domain.Validators;
[Registration] // Default Singleton lifetime
public class PetValidator : IPetValidator
{
public ValidationResult Validate(CreatePetRequest request)
{
var errors = new List<string>();
if (string.IsNullOrWhiteSpace(request.Name))
errors.Add("Pet name is required");
if (request.Age < 0)
errors.Add("Pet age must be positive");
return new ValidationResult(errors.Count == 0, errors);
}
}Handlers/IPetHandlers.cs:
namespace PetStore.Api.Handlers;
public interface IPetHandlers
{
void MapEndpoints(IEndpointRouteBuilder app);
}Handlers/PetHandlers.cs:
using Atc.DependencyInjection;
using PetStore.Domain.Models;
using PetStore.Domain.Services;
using PetStore.Domain.Validators;
namespace PetStore.Api.Handlers;
[Registration] // Default Singleton lifetime
public class PetHandlers : IPetHandlers
{
public void MapEndpoints(IEndpointRouteBuilder app)
{
var pets = app.MapGroup("/api/pets").WithTags("Pets");
pets.MapGet("/", GetAllPets)
.WithName("GetAllPets");
pets.MapGet("/{id:int}", GetPet)
.WithName("GetPet");
pets.MapPost("/", CreatePet)
.WithName("CreatePet");
pets.MapPut("/{id:int}", UpdatePet)
.WithName("UpdatePet");
pets.MapDelete("/{id:int}", DeletePet)
.WithName("DeletePet");
}
private static async Task<IResult> GetAllPets(IPetService petService)
{
var pets = await petService.GetAllPetsAsync();
return Results.Ok(pets);
}
private static async Task<IResult> GetPet(int id, IPetService petService)
{
var pet = await petService.GetPetAsync(id);
return pet is not null ? Results.Ok(pet) : Results.NotFound();
}
private static async Task<IResult> CreatePet(
CreatePetRequest request,
IPetService petService,
IPetValidator validator)
{
var validation = validator.Validate(request);
if (!validation.IsValid)
return Results.BadRequest(new { errors = validation.Errors });
var id = await petService.CreatePetAsync(request);
return Results.Created($"/api/pets/{id}", new { id });
}
private static async Task<IResult> UpdatePet(
int id,
UpdatePetRequest request,
IPetService petService)
{
var success = await petService.UpdatePetAsync(id, request);
return success ? Results.NoContent() : Results.NotFound();
}
private static async Task<IResult> DeletePet(int id, IPetService petService)
{
var success = await petService.DeletePetAsync(id);
return success ? Results.NoContent() : Results.NotFound();
}
}Option 1 - Manual Registration (Scenario A):
using Atc.DependencyInjection;
using PetStore.Api.Handlers;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Register all services from all three layers manually
builder.Services.AddDependencyRegistrationsFromDataAccess();
builder.Services.AddDependencyRegistrationsFromDomain();
builder.Services.AddDependencyRegistrationsFromApi();
var app = builder.Build();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// Map endpoints using handler
var petHandlers = app.Services.GetRequiredService<IPetHandlers>();
petHandlers.MapEndpoints(app);
app.Run();Option 2 - Transitive Registration (Scenario B - Recommended):
using Atc.DependencyInjection;
using PetStore.Api.Handlers;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// Single call registers all services from Domain + DataAccess automatically!
builder.Services.AddDependencyRegistrationsFromDomain(includeReferencedAssemblies: true);
// Only need to register API layer services separately
builder.Services.AddDependencyRegistrationsFromApi();
var app = builder.Build();
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
// Map endpoints using handler
var petHandlers = app.Services.GetRequiredService<IPetHandlers>();
petHandlers.MapEndpoints(app);
app.Run();For each project, the generator creates an assembly-specific extension method (with smart naming):
PetStore.DataAccess β AddDependencyRegistrationsFromDataAccess() (suffix "DataAccess" is unique)
services.AddScoped<IPetRepository, PetRepository>();PetStore.Domain β AddDependencyRegistrationsFromDomain() (suffix "Domain" is unique)
services.AddScoped<IPetService, PetService>();
services.AddSingleton<IPetValidator, PetValidator>();PetStore.Api β AddDependencyRegistrationsFromApi() (suffix "Api" is unique)
services.AddSingleton<IPetHandlers, PetHandlers>();Build and run:
dotnet run --project PetStore.ApiTest the endpoints:
# Get all pets
curl http://localhost:5265/api/pets/
# Get specific pet
curl http://localhost:5265/api/pets/1
# Create a new pet
curl -X POST http://localhost:5265/api/pets/ \
-H "Content-Type: application/json" \
-d '{"name":"Max","species":"Dog","age":2}'
# Test validation (should return errors)
curl -X POST http://localhost:5265/api/pets/ \
-H "Content-Type: application/json" \
-d '{"name":"","species":"Cat","age":-1}'
# Update a pet
curl -X PUT http://localhost:5265/api/pets/1 \
-H "Content-Type: application/json" \
-d '{"name":"Buddy Updated","species":"Dog","age":4}'
# Delete a pet
curl -X DELETE http://localhost:5265/api/pets/1Expected output:
- All endpoints work with proper dependency injection
- Validation errors return structured JSON:
{"errors":["Pet name is required","Pet age must be positive"]} - Logging appears in console for all service operations
To see what code the generator creates, add this to your .csproj files:
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)Generated</CompilerGeneratedFilesOutputPath>
</PropertyGroup>Generated files will appear in obj/Generated/Atc.SourceGenerators/:
RegistrationAttribute.g.cs- The[Registration]attribute definitionServiceCollectionExtensions.g.cs- TheAddDependencyRegistrationsFrom...()method
This quick guide demonstrated:
β
Zero boilerplate - No manual services.AddScoped<IFoo, Foo>() calls
β
Type safety - Compile-time validation of registrations
β
Auto-detection - Automatically registers against implemented interfaces
β
Multi-project support - Each assembly gets its own registration method
β
Flexible lifetimes - Singleton, Scoped, and Transient support
β
Clean architecture - Clear separation between layers
Compare this to manual registration:
// β Without Source Generator (manual, error-prone)
builder.Services.AddScoped<IPetRepository, PetRepository>();
builder.Services.AddScoped<IPetService, PetService>();
builder.Services.AddSingleton<IPetValidator, PetValidator>();
builder.Services.AddSingleton<IPetHandlers, PetHandlers>();
// ... and so on for every service
// β
With Source Generator (automatic, type-safe, smart naming)
builder.Services.AddDependencyRegistrationsFromDataAccess();
builder.Services.AddDependencyRegistrationsFromDomain();
builder.Services.AddDependencyRegistrationsFromApi();Add the NuGet package to each project that contains services to register:
Required:
dotnet add package Atc.SourceGeneratorsOptional (recommended for better IntelliSense):
dotnet add package Atc.SourceGenerators.AnnotationsOr add to your .csproj file:
<ItemGroup>
<!-- Required: Source generator -->
<PackageReference Include="Atc.SourceGenerators" Version="1.0.0" />
<!-- Optional: Attribute definitions with XML documentation -->
<PackageReference Include="Atc.SourceGenerators.Annotations" Version="1.0.0" />
</ItemGroup>Note: The generator emits fallback attributes automatically, so the Annotations package is optional. However, it provides better XML documentation and IntelliSense. If you include it, suppress the expected CS0436 warning: <NoWarn>$(NoWarn);CS0436</NoWarn>
The generator runs at compile time and has zero runtime overhead.
using Atc.DependencyInjection;// Simple registration with default singleton lifetime
[Registration]
public class CacheService
{
public void Store(string key, string value) { }
}
// Interface auto-detection
[Registration]
public class UserService : IUserService
{
public void CreateUser(string name) { }
}
// Scoped lifetime
[Registration(Lifetime.Scoped)]
public class OrderService : IOrderService
{
public void ProcessOrder(int orderId) { }
}
// Transient lifetime
[Registration(Lifetime.Transient)]
public class LoggerService
{
public void Log(string message) { }
}using Microsoft.Extensions.DependencyInjection;
var services = new ServiceCollection();
// Call the generated extension method
services.AddDependencyRegistrationsFromYourAssemblyName();
var serviceProvider = services.BuildServiceProvider();- Automatic Service Registration: Decorate classes with
[Registration]attribute for automatic DI registration - Generic Interface Registration: Full support for open generic types like
IRepository<T>andIHandler<TRequest, TResponse>π - Keyed Service Registration: Multiple implementations of the same interface with different keys (.NET 8+)
- Factory Method Registration: Custom initialization logic via static factory methods
- TryAdd Registration*: Conditional registration for default implementations (library pattern)
- Conditional Registration: Register services based on configuration values (feature flags, environment-specific services) π
- Assembly Scanning Filters: Exclude types by namespace, pattern (wildcards), or interface implementation
- Runtime Filtering: Exclude services at registration time with method parameters (different apps, different service subsets) π
- Hosted Service Support: Automatically detects
BackgroundServiceandIHostedServiceimplementations and usesAddHostedService<T>() - Interface Auto-Detection: Automatically registers against all implemented interfaces (no
Asparameter needed!) - Smart Filtering: System interfaces (IDisposable, etc.) are automatically excluded
- Multiple Interface Support: Services implementing multiple interfaces are registered against all of them
- Flexible Lifetimes: Support for Singleton, Scoped, and Transient service lifetimes
- Explicit Override: Optional
Asparameter to override auto-detection when needed - Dual Registration: Register services as both interface and concrete type with
AsSelf - Compile-time Validation: Diagnostics for common errors (invalid interface types, missing implementations, incorrect hosted service lifetimes)
- Zero Runtime Overhead: All code is generated at compile time
- Native AOT Compatible: No reflection or runtime code generation - fully trimming-safe and AOT-ready
- Multi-Project Support: Each project generates its own registration method
When using the generator across multiple projects, each project generates its own extension method with a unique name based on the assembly name.
Solution/
βββ MyApp.Api/ β AddDependencyRegistrationsFromApi()
βββ MyApp.Domain/ β AddDependencyRegistrationsFromDomain()
βββ MyApp.DataAccess/ β AddDependencyRegistrationsFromDataAccess()
Scenario A - Manual Registration (Traditional):
Call all registration methods explicitly in your startup:
var builder = WebApplication.CreateBuilder(args);
// Register services from all projects manually
builder.Services.AddDependencyRegistrationsFromDataAccess();
builder.Services.AddDependencyRegistrationsFromDomain();
builder.Services.AddDependencyRegistrationsFromApi();
var app = builder.Build();Scenario B - Transitive Registration (Recommended):
Let the generator automatically register services from referenced assemblies:
var builder = WebApplication.CreateBuilder(args);
// Single call automatically registers:
// - Services from MyApp.Domain
// - Services from MyApp.DataAccess (referenced by Domain)
builder.Services.AddDependencyRegistrationsFromDomain(includeReferencedAssemblies: true);
var app = builder.Build();Each generated registration method has 4 overloads to support different registration scenarios:
// Overload 1: Default (no transitive registration)
builder.Services.AddDependencyRegistrationsFromDomain();
// Overload 2: Auto-detect ALL referenced assemblies recursively
builder.Services.AddDependencyRegistrationsFromDomain(includeReferencedAssemblies: true);
// Overload 3: Register specific referenced assembly by name (short or full name)
builder.Services.AddDependencyRegistrationsFromDomain("DataAccess");
builder.Services.AddDependencyRegistrationsFromDomain("MyApp.DataAccess");
// Overload 4: Register multiple specific referenced assemblies
builder.Services.AddDependencyRegistrationsFromDomain("DataAccess", "Infrastructure");How It Works:
-
Auto-detect mode (
includeReferencedAssemblies: true):- Scans ALL referenced assemblies that contain
[Registration]attributes - Recursively registers services from the entire dependency chain
- Example: Domain β DataAccess β Infrastructure (all registered with ONE call)
- Scans ALL referenced assemblies that contain
-
Manual mode (assembly name parameters):
- Only registers assemblies with matching prefix (e.g., "MyApp.*")
- Supports both full names ("MyApp.DataAccess") and short names ("DataAccess")
- Allows fine-grained control over which assemblies to include
-
Silent skip:
- If a specified assembly doesn't exist or has no registrations, it's silently skipped
- No compile-time or runtime errors
Benefits:
β Clean Architecture: Api β Domain β DataAccess (no need for Api to reference DataAccess directly) β Less Boilerplate: One registration call instead of many β Maintainable: Adding new projects doesn't require updating Program.cs β Type-Safe: All registrations validated at compile time
The generator creates extension methods based on assembly names with smart naming:
| Assembly Name | Smart Naming Result |
|---|---|
PetStore.Domain (unique suffix) |
AddDependencyRegistrationsFromDomain() |
PetStore.DataAccess (unique suffix) |
AddDependencyRegistrationsFromDataAccess() |
PetStore.Domain + AnotherApp.Domain |
AddDependencyRegistrationsFromPetStoreDomain() and AddDependencyRegistrationsFromAnotherAppDomain() |
Assembly names are sanitized to create valid C# identifiers (dots, dashes, spaces removed).
The generator uses smart suffix-based naming to create cleaner, more readable method names:
How it works:
- β If the assembly suffix (last segment after final dot) is unique among all assemblies β use short suffix
β οΈ If multiple assemblies have the same suffix β use full sanitized name to avoid conflicts
Examples:
// β
Unique suffixes (cleaner names)
PetStore.Domain β AddDependencyRegistrationsFromDomain()
PetStore.DataAccess β AddDependencyRegistrationsFromDataAccess()
PetStore.Api β AddDependencyRegistrationsFromApi()
// β οΈ Conflicting suffixes (full names prevent collisions)
PetStore.Domain β AddDependencyRegistrationsFromPetStoreDomain()
AnotherApp.Domain β AddDependencyRegistrationsFromAnotherAppDomain()Benefits:
- π― Cleaner API: Shorter method names when there are no conflicts
- π‘οΈ Automatic Conflict Prevention: Fallback to full names prevents naming collisions
- β‘ Zero Configuration: Works automatically based on compilation context
- π Context-Aware: Method names adapt to the assemblies in your solution
The generator automatically detects and registers services against their implemented interfaces:
public interface IUserService { }
[Registration]
public class UserService : IUserService { }Generated:
services.AddSingleton<IUserService, UserService>();public interface IEmailService { }
public interface INotificationService { }
[Registration]
public class EmailService : IEmailService, INotificationService { }Generated:
services.AddSingleton<IEmailService, EmailService>();
services.AddSingleton<INotificationService, EmailService>();System and Microsoft namespace interfaces are automatically excluded:
[Registration]
public class CacheService : IDisposable
{
public void Dispose() { }
}Generated:
services.AddSingleton<CacheService>(); // IDisposable ignoredUse As parameter to register against a specific interface only:
[Registration(As = typeof(IUserService))]
public class UserService : IUserService, INotificationService { }Generated:
services.AddSingleton<IUserService, UserService>(); // Only IUserServiceUse AsSelf = true to register both ways:
[Registration(AsSelf = true)]
public class EmailService : IEmailService { }Generated:
services.AddSingleton<IEmailService, EmailService>();
services.AddSingleton<EmailService>();Single instance for the entire application lifetime:
[Registration] // or [Registration(Lifetime.Singleton)]
public class CacheService { }Use for: Stateless services, shared state, expensive-to-create objects
New instance per scope (e.g., per HTTP request):
[Registration(Lifetime.Scoped)]
public class OrderService : IOrderService { }Use for: Database contexts, request-specific services, unit of work pattern
New instance every time it's requested:
[Registration(Lifetime.Transient)]
public class LoggerService { }Use for: Lightweight, stateless services, services that shouldn't be shared
The Dependency Registration Generator is fully compatible with Native AOT compilation, making it ideal for modern .NET applications that require fast startup times and minimal deployment sizes.
All registration code is generated at compile time with no reflection or runtime code generation:
// Your attributed service
[Registration(Lifetime.Scoped)]
public class UserService : IUserService { }
// Generated code (compile-time, no reflection!)
public static class DependencyRegistrationExtensions
{
public static IServiceCollection AddDependencyRegistrationsFromApp(
this IServiceCollection services)
{
services.AddScoped<IUserService, UserService>();
return services;
}
}- β No reflection required: All service registration happens through direct method calls
- β Fully trimming-safe: No dynamic code means the trimmer can safely remove unused code
- β AOT-ready: All dependencies are resolved at compile time
- β Faster startup: No runtime scanning or reflection overhead
- β Smaller deployments: Dead code elimination works perfectly
- β Better performance: Native code execution with zero runtime overhead
<!-- YourProject.csproj -->
<PropertyGroup>
<PublishAot>true</PublishAot>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Atc.SourceGenerators" Version="1.0.0" />
</ItemGroup>// Your services work seamlessly with Native AOT
[Registration(Lifetime.Singleton)]
public class CacheService : ICacheService { }
[Registration(Lifetime.Scoped)]
public class UserService : IUserService { }
// Program.cs
var builder = WebApplication.CreateSlimBuilder(args);
builder.Services.AddDependencyRegistrationsFromApp();
var app = builder.Build();Result: Fast startup, minimal memory footprint, and production-ready native binaries.
| Parameter | Type | Default | Description |
|---|---|---|---|
lifetime |
Lifetime |
Singleton |
Service lifetime (Singleton, Scoped, or Transient) |
As |
Type? |
null |
Explicit interface type to register against (overrides auto-detection) |
AsSelf |
bool |
false |
Also register the concrete type when interfaces are detected/specified |
Key |
object? |
null |
Service key for keyed service registration (.NET 8+) |
Factory |
string? |
null |
Name of static factory method for custom initialization |
TryAdd |
bool |
false |
Use TryAdd* methods for conditional registration (library pattern) |
Decorator |
bool |
false |
Mark this service as a decorator that wraps the previous registration of the same interface |
Condition |
string? |
null |
Configuration key path for conditional registration (feature flags). Prefix with "!" for negation |
// Default singleton, auto-detect interfaces
[Registration]
// Explicit lifetime
[Registration(Lifetime.Scoped)]
// Explicit interface
[Registration(As = typeof(IUserService))]
// Combination
[Registration(Lifetime.Transient, As = typeof(ILogger))]
// Also register concrete type
[Registration(AsSelf = true)]
// Keyed service
[Registration(Key = "Primary", As = typeof(ICache))]
// Factory method
[Registration(Factory = nameof(Create))]
// TryAdd registration
[Registration(TryAdd = true)]
// Decorator pattern
[Registration(Lifetime.Scoped, As = typeof(IOrderService), Decorator = true)]
// Conditional registration
[Registration(As = typeof(ICache), Condition = "Features:UseRedisCache")]
// Negated conditional
[Registration(As = typeof(ICache), Condition = "!Features:UseRedisCache")]
// All parameters
[Registration(Lifetime.Scoped, As = typeof(IService), AsSelf = true, TryAdd = true)]The generator provides compile-time diagnostics to catch common errors:
Severity: Error
Description: The type specified in As parameter must be an interface or abstract class.
// β Error: BaseService is a concrete class
public class BaseService { }
[Registration(As = typeof(BaseService))]
public class UserService : BaseService { }
// β
OK: Abstract base class
public abstract class AbstractBaseService { }
[Registration(As = typeof(AbstractBaseService))]
public class UserService : AbstractBaseService { }
// β
OK: Interface
public interface IUserService { }
[Registration(As = typeof(IUserService))]
public class UserService : IUserService { }Fix: Use an interface, abstract class, or remove the As parameter.
Severity: Error
Description: Class does not implement the interface or inherit from the abstract class specified in As parameter.
public interface IUserService { }
// β Error: UserService doesn't implement IUserService
[Registration(As = typeof(IUserService))]
public class UserService { }
public abstract class AuthenticationStateProvider { }
// β Error: UserService doesn't inherit from AuthenticationStateProvider
[Registration(As = typeof(AuthenticationStateProvider))]
public class UserService { }Fix: Implement the interface, inherit from the abstract class, or remove the As parameter.
Severity: Warning
Description: Service is registered multiple times with different lifetimes.
public interface IUserService { }
// β οΈ Warning: Same interface registered with two different lifetimes
[Registration(Lifetime.Singleton)]
public class UserServiceSingleton : IUserService { }
[Registration(Lifetime.Scoped)]
public class UserServiceScoped : IUserService { }Fix: Ensure consistent lifetimes or use different interfaces.
Severity: Error
Description: Hosted services (BackgroundService or IHostedService implementations) must use Singleton lifetime.
// β Error: Hosted services cannot use Scoped or Transient lifetime
[Registration(Lifetime.Scoped)]
public class MyBackgroundService : BackgroundService
{
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
return Task.CompletedTask;
}
}Fix: Use Singleton lifetime (or default [Registration]) for hosted services:
// β
Correct: Singleton lifetime (explicit)
[Registration(Lifetime.Singleton)]
public class MyBackgroundService : BackgroundService { }
// β
Correct: Default lifetime is Singleton
[Registration]
public class MyBackgroundService : BackgroundService { }Generated Registration:
services.AddHostedService<MyBackgroundService>();The generator supports open generic types, enabling the repository pattern and other generic service patterns.
// Generic interface
public interface IRepository<T> where T : class
{
T? GetById(int id);
IEnumerable<T> GetAll();
void Add(T entity);
}
// Generic implementation
[Registration(Lifetime.Scoped)]
public class Repository<T> : IRepository<T> where T : class
{
public T? GetById(int id) => /* implementation */;
public IEnumerable<T> GetAll() => /* implementation */;
public void Add(T entity) => /* implementation */;
}Generated Code:
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));Usage:
// Resolve for specific types
var userRepository = serviceProvider.GetRequiredService<IRepository<User>>();
var productRepository = serviceProvider.GetRequiredService<IRepository<Product>>();// Handler interface with two type parameters
public interface IHandler<TRequest, TResponse>
{
TResponse Handle(TRequest request);
}
[Registration(Lifetime.Transient)]
public class Handler<TRequest, TResponse> : IHandler<TRequest, TResponse>
{
public TResponse Handle(TRequest request) => /* implementation */;
}Generated Code:
services.AddTransient(typeof(IHandler<,>), typeof(Handler<,>));// Interface with multiple constraints
public interface IRepository<T>
where T : class, IEntity, new()
{
T Create();
void Save(T entity);
}
[Registration(Lifetime.Scoped)]
public class Repository<T> : IRepository<T>
where T : class, IEntity, new()
{
public T Create() => new T();
public void Save(T entity) => /* implementation */;
}Generated Code:
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));You can also use explicit As parameter with open generic types:
[Registration(Lifetime.Scoped, As = typeof(IRepository<>))]
public class Repository<T> : IRepository<T> where T : class
{
// Implementation
}Register multiple implementations of the same interface and resolve them by key (.NET 8+).
[Registration(Lifetime.Scoped, As = typeof(IPaymentProcessor), Key = "Stripe")]
public class StripePaymentProcessor : IPaymentProcessor
{
public Task ProcessPaymentAsync(decimal amount) { /* Stripe implementation */ }
}
[Registration(Lifetime.Scoped, As = typeof(IPaymentProcessor), Key = "PayPal")]
public class PayPalPaymentProcessor : IPaymentProcessor
{
public Task ProcessPaymentAsync(decimal amount) { /* PayPal implementation */ }
}Generated Code:
services.AddKeyedScoped<IPaymentProcessor, StripePaymentProcessor>("Stripe");
services.AddKeyedScoped<IPaymentProcessor, PayPalPaymentProcessor>("PayPal");Usage:
// Constructor injection with [FromKeyedServices]
public class CheckoutService(
[FromKeyedServices("Stripe")] IPaymentProcessor stripeProcessor,
[FromKeyedServices("PayPal")] IPaymentProcessor paypalProcessor)
{
// Use specific implementations
}
// Manual resolution
var stripeProcessor = serviceProvider.GetRequiredKeyedService<IPaymentProcessor>("Stripe");
var paypalProcessor = serviceProvider.GetRequiredKeyedService<IPaymentProcessor>("PayPal");Keyed services work with generic types:
[Registration(Lifetime.Scoped, As = typeof(IRepository<>), Key = "Primary")]
public class PrimaryRepository<T> : IRepository<T> where T : class
{
public T? GetById(int id) => /* Primary database */;
}
[Registration(Lifetime.Scoped, As = typeof(IRepository<>), Key = "ReadOnly")]
public class ReadOnlyRepository<T> : IRepository<T> where T : class
{
public T? GetById(int id) => /* Read-only replica */;
}Generated Code:
services.AddKeyedScoped(typeof(IRepository<>), "Primary", typeof(PrimaryRepository<>));
services.AddKeyedScoped(typeof(IRepository<>), "ReadOnly", typeof(ReadOnlyRepository<>));Factory methods allow custom initialization logic for services that require configuration values, conditional setup, or complex dependencies.
[Registration(Lifetime.Scoped, As = typeof(IEmailSender), Factory = nameof(CreateEmailSender))]
public class EmailSender : IEmailSender
{
private readonly string smtpHost;
private readonly int smtpPort;
private EmailSender(string smtpHost, int smtpPort)
{
this.smtpHost = smtpHost;
this.smtpPort = smtpPort;
}
public Task SendEmailAsync(string to, string subject, string body)
{
// Implementation...
}
/// <summary>
/// Factory method for creating EmailSender instances.
/// Must be static and accept IServiceProvider as parameter.
/// </summary>
public static IEmailSender CreateEmailSender(IServiceProvider serviceProvider)
{
// Resolve configuration from DI container
var config = serviceProvider.GetRequiredService<IConfiguration>();
var smtpHost = config["Email:SmtpHost"] ?? "smtp.example.com";
var smtpPort = int.Parse(config["Email:SmtpPort"] ?? "587");
return new EmailSender(smtpHost, smtpPort);
}
}Generated Code:
services.AddScoped<IEmailSender>(sp => EmailSender.CreateEmailSender(sp));- β
Must be
static - β
Must accept
IServiceProvideras the single parameter - β
Must return the service type (interface specified in
Asparameter, or class type if noAsspecified) - β
Can be
public,internal, orprivate
[Registration(Lifetime.Singleton, Factory = nameof(CreateService))]
public class CacheService : ICacheService, IHealthCheck
{
private readonly string connectionString;
private CacheService(string connectionString)
{
this.connectionString = connectionString;
}
public static ICacheService CreateService(IServiceProvider sp)
{
var config = sp.GetRequiredService<IConfiguration>();
var connString = config.GetConnectionString("Redis");
return new CacheService(connString);
}
// ICacheService members...
// IHealthCheck members...
}Generated Code:
// Registers against both interfaces using the same factory
services.AddSingleton<ICacheService>(sp => CacheService.CreateService(sp));
services.AddSingleton<IHealthCheck>(sp => CacheService.CreateService(sp));When to Use Factory Methods:
- β
Service requires configuration values from
IConfiguration - β Conditional initialization based on runtime environment
- β Complex dependency resolution beyond constructor injection
- β Services with private constructors that require initialization
When NOT to Use Factory Methods:
- β Simple services with no special initialization - use regular constructor injection
- β Services that can use
IOptions<T>pattern instead - β When factory logic is overly complex - consider using a dedicated factory class
The generator provides compile-time validation:
ATCDIR005: Factory method not found
// β Error: Factory method doesn't exist
[Registration(Factory = "NonExistentMethod")]
public class MyService : IMyService { }ATCDIR006: Invalid factory method signature
// β Error: Factory method must be static
[Registration(Factory = nameof(Create))]
public class MyService : IMyService
{
public IMyService Create(IServiceProvider sp) => this; // Not static!
}
// β Error: Wrong parameter type
public static IMyService Create(string config) => new MyService();
// β Error: Wrong return type
public static string Create(IServiceProvider sp) => "wrong";Correct signature:
// β
Correct: static, accepts IServiceProvider, returns service type
public static IMyService Create(IServiceProvider sp) => new MyService();Instance registration allows you to register pre-created singleton instances via static fields, properties, or methods. This is ideal for immutable configuration objects or singleton instances that are initialized at startup.
[Registration(As = typeof(IAppConfiguration), Instance = nameof(DefaultInstance))]
public class AppConfiguration : IAppConfiguration
{
// Static field providing the pre-created instance
public static readonly AppConfiguration DefaultInstance = new()
{
ApplicationName = "My Application",
Environment = "Production",
MaxConnections = 100,
IsDebugMode = false,
};
// Private constructor enforces singleton pattern
private AppConfiguration() { }
public string ApplicationName { get; init; } = string.Empty;
public string Environment { get; init; } = string.Empty;
public int MaxConnections { get; init; }
public bool IsDebugMode { get; init; }
}Generated Code:
services.AddSingleton<IAppConfiguration>(AppConfiguration.DefaultInstance);[Registration(As = typeof(ICache), Instance = nameof(Instance))]
public class MemoryCache : ICache
{
// Static property providing the singleton instance
public static MemoryCache Instance { get; } = new MemoryCache();
private MemoryCache() { }
public void Set(string key, object value) { /* implementation */ }
public object? Get(string key) { /* implementation */ }
}Generated Code:
services.AddSingleton<ICache>(MemoryCache.Instance);[Registration(As = typeof(ILogger), Instance = nameof(GetDefaultLogger))]
public class DefaultLogger : ILogger
{
private static readonly DefaultLogger instance = new();
private DefaultLogger() { }
// Static method returning the singleton instance
public static DefaultLogger GetDefaultLogger() => instance;
public void Log(string message) => Console.WriteLine($"[{DateTime.UtcNow:O}] {message}");
}Generated Code:
services.AddSingleton<ILogger>(DefaultLogger.GetDefaultLogger());- β
The
Instanceparameter must reference a static field, property, or parameterless method - β Instance registration requires Singleton lifetime (enforced at compile-time)
- β
The member can be
public,internal, orprivate - β
Works with
TryAdd:services.TryAddSingleton<T>(ClassName.Instance) - β Cannot be used with
Factoryparameter (mutually exclusive) - β Cannot be used with Scoped or Transient lifetimes
When a class implements multiple interfaces, the same instance is registered for each interface:
[Registration(Instance = nameof(DefaultInstance))]
public class ServiceHub : IServiceA, IServiceB, IHealthCheck
{
public static readonly ServiceHub DefaultInstance = new();
private ServiceHub() { }
// IServiceA members...
// IServiceB members...
// IHealthCheck members...
}Generated Code:
// Same instance registered for all interfaces
services.AddSingleton<IServiceA>(ServiceHub.DefaultInstance);
services.AddSingleton<IServiceB>(ServiceHub.DefaultInstance);
services.AddSingleton<IHealthCheck>(ServiceHub.DefaultInstance);When to Use Instance Registration:
- β Immutable configuration objects with default values
- β Pre-initialized singleton services (caches, registries, etc.)
- β Singleton instances that should be shared across the application
- β Services with complex initialization that should happen once at startup
When NOT to Use Instance Registration:
- β Services that need different instances per scope/request - use factory methods instead
- β Services requiring runtime configuration - use
IOptions<T>pattern or factory methods - β Services with mutable state that shouldn't be shared
- β Non-singleton lifetimes - instance registration only supports Singleton
The generator provides compile-time validation:
β ATCDIR007: Instance member not found
[Registration(As = typeof(ICache), Instance = "NonExistentMember")]
public class CacheService : ICache { }
// Error: Member 'NonExistentMember' not found. Must be a static field, property, or method.β ATCDIR008: Instance member must be static
[Registration(As = typeof(ICache), Instance = nameof(InstanceField))]
public class CacheService : ICache
{
public readonly CacheService InstanceField = new(); // Not static!
}
// Error: Instance member 'InstanceField' must be static.β ATCDIR009: Instance and Factory are mutually exclusive
[Registration(
As = typeof(IEmailSender),
Factory = nameof(Create),
Instance = nameof(DefaultInstance))] // Cannot use both!
public class EmailSender : IEmailSender { }
// Error: Cannot use both Instance and Factory parameters on the same service.β ATCDIR010: Instance registration requires Singleton lifetime
[Registration(Lifetime.Scoped, As = typeof(ICache), Instance = nameof(Instance))]
public class CacheService : ICache
{
public static CacheService Instance { get; } = new();
}
// Error: Instance registration can only be used with Singleton lifetime. Current lifetime is 'Scoped'.Use Instance when:
- The instance is pre-created and immutable
- No runtime dependencies or configuration needed
- Singleton pattern with guaranteed single instance
Use Factory Method when:
- Service requires
IServiceProviderto resolve dependencies - Runtime configuration is needed (from
IConfiguration, environment, etc.) - Conditional initialization based on runtime state
Example comparison:
// Instance: Pre-created, immutable configuration
[Registration(As = typeof(IAppSettings), Instance = nameof(Default))]
public class AppSettings : IAppSettings
{
public static readonly AppSettings Default = new() { Timeout = 30 };
}
// Factory: Runtime configuration from IConfiguration
[Registration(Lifetime.Singleton, As = typeof(IEmailClient), Factory = nameof(Create))]
public class EmailClient : IEmailClient
{
public static IEmailClient Create(IServiceProvider sp)
{
var config = sp.GetRequiredService<IConfiguration>();
var apiKey = config["Email:ApiKey"];
return new EmailClient(apiKey);
}
}TryAdd* registration enables conditional service registration that only adds services if they're not already registered. This is particularly useful for library authors who want to provide default implementations that can be easily overridden by application code.
[Registration(As = typeof(ILogger), TryAdd = true)]
public class DefaultLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"[DefaultLogger] {message}");
}
}Generated Code:
services.TryAddSingleton<ILogger, DefaultLogger>();When TryAdd = true, the generator uses TryAdd{Lifetime}() methods instead of Add{Lifetime}():
TryAddSingleton<T>()- Only registers if noTis already registeredTryAddScoped<T>()- Only registers if noTis already registeredTryAddTransient<T>()- Only registers if noTis already registered
This allows consumers to override default implementations:
// Application code (runs BEFORE library registration)
services.AddSingleton<ILogger, CustomLogger>(); // This takes precedence
// Library registration (uses TryAdd)
services.AddDependencyRegistrationsFromLibrary();
// DefaultLogger will NOT be registered because CustomLogger is already registeredTryAdd is perfect for libraries that want to provide sensible defaults:
// Library code: PetStore.Domain
namespace PetStore.Domain.Services;
[Registration(Lifetime.Singleton, As = typeof(IHealthCheck), TryAdd = true)]
public class DefaultHealthCheck : IHealthCheck
{
public Task<bool> CheckHealthAsync()
{
Console.WriteLine("DefaultHealthCheck: Performing basic health check (always healthy)");
return Task.FromResult(true);
}
}Consumer can override:
// Application code
services.AddSingleton<IHealthCheck, AdvancedHealthCheck>(); // Custom implementation
services.AddDependencyRegistrationsFromDomain(); // DefaultHealthCheck won't be addedOr consumer can use default:
// Application code
services.AddDependencyRegistrationsFromDomain(); // DefaultHealthCheck is added// Scoped with TryAdd
[Registration(Lifetime.Scoped, As = typeof(ICache), TryAdd = true)]
public class DefaultCache : ICache
{
public string Get(string key) => /* implementation */;
}
// Transient with TryAdd
[Registration(Lifetime.Transient, As = typeof(IMessageFormatter), TryAdd = true)]
public class DefaultMessageFormatter : IMessageFormatter
{
public string Format(string message) => /* implementation */;
}Generated Code:
services.TryAddScoped<ICache, DefaultCache>();
services.TryAddTransient<IMessageFormatter, DefaultMessageFormatter>();TryAdd works seamlessly with factory methods:
[Registration(Lifetime.Singleton, As = typeof(IEmailSender), TryAdd = true, Factory = nameof(CreateEmailSender))]
public class DefaultEmailSender : IEmailSender
{
private readonly string smtpHost;
private DefaultEmailSender(string smtpHost)
{
this.smtpHost = smtpHost;
}
public static IEmailSender CreateEmailSender(IServiceProvider provider)
{
var config = provider.GetRequiredService<IConfiguration>();
var host = config["Email:SmtpHost"] ?? "localhost";
return new DefaultEmailSender(host);
}
public Task SendEmailAsync(string to, string subject, string body)
{
// Implementation...
}
}Generated Code:
services.TryAddSingleton<IEmailSender>(sp => DefaultEmailSender.CreateEmailSender(sp));TryAdd supports generic interface registration:
[Registration(Lifetime.Scoped, TryAdd = true)]
public class DefaultRepository<T> : IRepository<T> where T : class
{
public T? GetById(int id) => /* implementation */;
public IEnumerable<T> GetAll() => /* implementation */;
}Generated Code:
services.TryAddScoped(typeof(IRepository<>), typeof(DefaultRepository<>));When a service implements multiple interfaces, TryAdd is applied to each registration:
[Registration(TryAdd = true)]
public class DefaultNotificationService : IEmailNotificationService, ISmsNotificationService
{
public Task SendEmailAsync(string email, string message) => /* implementation */;
public Task SendSmsAsync(string phoneNumber, string message) => /* implementation */;
}Generated Code:
services.TryAddSingleton<IEmailNotificationService, DefaultNotificationService>();
services.TryAddSingleton<ISmsNotificationService, DefaultNotificationService>();When to Use TryAdd:
- β Library projects providing default implementations
- β Fallback services that applications may want to customize
- β Services with sensible defaults but customizable behavior
- β Avoiding registration conflicts in modular applications
When NOT to Use TryAdd:
- β Core application services that should always be registered
- β Services where registration order matters for business logic
- β When you need to explicitly override existing registrations (use regular registration)
Keyed Services:
TryAdd is not supported with keyed services. When both Key and TryAdd are specified, the generator will prioritize keyed registration and ignore TryAdd:
// β οΈ TryAdd is ignored when Key is specified
[Registration(Key = "Primary", TryAdd = true)]
public class PrimaryCache : ICache { }
// Generated (keyed registration, no TryAdd):
services.AddKeyedSingleton<ICache, PrimaryCache>("Primary");Registration Order: For TryAdd to work correctly, ensure library registrations happen after application-specific registrations:
// β
Correct order
services.AddSingleton<ILogger, CustomLogger>(); // Application override
services.AddDependencyRegistrationsFromLibrary(); // Library defaults (TryAdd)
// β Wrong order
services.AddDependencyRegistrationsFromLibrary(); // Library defaults register first
services.AddSingleton<ILogger, CustomLogger>(); // This creates a duplicate registration!Assembly Scanning Filters allow you to exclude specific types, namespaces, or patterns from automatic registration. This is particularly useful for:
- Excluding internal/test services from production builds
- Preventing mock/stub services from being registered
- Filtering out utilities that shouldn't be in the DI container
Filters are applied using the [RegistrationFilter] attribute at the assembly level:
using Atc.DependencyInjection;
// Exclude internal services
[assembly: RegistrationFilter(ExcludeNamespaces = new[] { "MyApp.Internal" })]
// Your services
namespace MyApp.Services
{
[Registration]
public class ProductionService : IProductionService { } // β
Will be registered
}
namespace MyApp.Internal
{
[Registration]
public class InternalService : IInternalService { } // β Excluded by filter
}Exclude types in specific namespaces. Sub-namespaces are also excluded:
[assembly: RegistrationFilter(ExcludeNamespaces = new[] {
"MyApp.Internal",
"MyApp.Testing",
"MyApp.Utilities"
})]
namespace MyApp.Services
{
[Registration]
public class UserService : IUserService { } // β
Registered
}
namespace MyApp.Internal
{
[Registration]
public class InternalCache : ICache { } // β Excluded
}
namespace MyApp.Internal.Deep.Nested
{
[Registration]
public class DeepService : IDeepService { } // β Also excluded (sub-namespace)
}How Namespace Filtering Works:
- Exact match:
"MyApp.Internal"excludes types in that namespace - Sub-namespace match: Also excludes
"MyApp.Internal.Something","MyApp.Internal.Deep.Nested", etc.
Exclude types whose names match wildcard patterns. Supports * (any characters) and ? (single character):
[assembly: RegistrationFilter(ExcludePatterns = new[] {
"*Mock*", // Excludes MockEmailService, EmailMockService, etc.
"*Test*", // Excludes TestHelper, UserTestService, etc.
"Temp*", // Excludes TempService, TempCache, etc.
"Old?Data" // Excludes OldAData, OldBData, but NOT OldAbcData
})]
namespace MyApp.Services
{
[Registration]
public class ProductionEmailService : IEmailService { } // β
Registered
[Registration]
public class MockEmailService : IEmailService { } // β Excluded (*Mock*)
[Registration]
public class UserTestHelper : ITestHelper { } // β Excluded (*Test*)
[Registration]
public class TempCache : ICache { } // β Excluded (Temp*)
}Pattern Matching Rules:
*matches zero or more characters?matches exactly one character- Matching is case-insensitive
- Patterns match against the type name (not the full namespace)
Exclude types that implement specific interfaces:
[assembly: RegistrationFilter(ExcludeImplementing = new[] {
typeof(ITestUtility),
typeof(IInternalTool)
})]
namespace MyApp.Services
{
public interface ITestUtility { }
public interface IProductionService { }
[Registration]
public class ProductionService : IProductionService { } // β
Registered
[Registration]
public class TestHelper : ITestUtility { } // β Excluded (implements ITestUtility)
[Registration]
public class MockDatabase : IDatabase, ITestUtility { } // β Excluded (implements ITestUtility)
}How Interface Filtering Works:
- Checks all interfaces implemented by the type
- Uses proper generic type comparison (
SymbolEqualityComparer) - Works with generic interfaces like
IRepository<T>
You can combine multiple filter types in a single attribute:
[assembly: RegistrationFilter(
ExcludeNamespaces = new[] { "MyApp.Internal", "MyApp.Testing" },
ExcludePatterns = new[] { "*Mock*", "*Test*", "*Fake*" },
ExcludeImplementing = new[] { typeof(ITestUtility) })]
// All filter rules are applied
// A type is excluded if it matches ANY of the rulesYou can also apply multiple [RegistrationFilter] attributes:
// Filter 1: Exclude internal namespaces
[assembly: RegistrationFilter(ExcludeNamespaces = new[] {
"MyApp.Internal"
})]
// Filter 2: Exclude test patterns
[assembly: RegistrationFilter(ExcludePatterns = new[] {
"*Mock*",
"*Test*"
})]
// Filter 3: Exclude utility interfaces
[assembly: RegistrationFilter(ExcludeImplementing = new[] {
typeof(ITestUtility)
})]
// All filters are combinedHere's a complete example showing filters in action:
// AssemblyInfo.cs
using Atc.DependencyInjection;
[assembly: RegistrationFilter(
ExcludeNamespaces = new[] {
"MyApp.Internal",
"MyApp.Development"
},
ExcludePatterns = new[] {
"*Mock*",
"*Test*",
"*Fake*",
"Temp*"
})]// Production service - WILL be registered
namespace MyApp.Services
{
[Registration]
public class EmailService : IEmailService
{
public void SendEmail(string to, string message) { }
}
}
// Internal service - EXCLUDED by namespace
namespace MyApp.Internal
{
[Registration]
public class InternalCache : ICache { } // β Excluded
}
// Mock service - EXCLUDED by pattern
namespace MyApp.Services
{
[Registration]
public class MockEmailService : IEmailService { } // β Excluded
}
// Test helper - EXCLUDED by pattern
namespace MyApp.Testing
{
[Registration]
public class TestDataBuilder : ITestHelper { } // β Excluded
}Important Notes:
- Filters are applied first: Types are filtered OUT before any registration happens
- ANY match excludes: If a type matches ANY filter rule, it's excluded
- Applies to all registrations: Filters affect both current assembly and referenced assemblies
- No diagnostics for filtered types: Filtered types are silently skipped (this is intentional)
You can verify filters are working by trying to resolve filtered services:
var services = new ServiceCollection();
services.AddDependencyRegistrationsFromMyApp();
var provider = services.BuildServiceProvider();
// This will return null (service was filtered out)
var mockService = provider.GetService<IMockEmailService>();
Console.WriteLine($"MockEmailService registered: {mockService != null}"); // False
// This will succeed (service was not filtered)
var emailService = provider.GetRequiredService<IEmailService>();
Console.WriteLine($"EmailService registered: {emailService != null}"); // TrueWhen to Use Filters:
- β Excluding internal implementation details from DI
- β Preventing test/mock services from production builds
- β Filtering development-only utilities
- β Clean separation between production and development code
When NOT to Use Filters:
- β Don't use filters as the primary way to control registration (use conditional compilation instead)
- β Don't create overly complex filter patterns that are hard to understand
- β Don't filter services that SHOULD be in DI but you forgot to configure properly
Recommended Patterns:
// β
Good: Clear, specific exclusions
[assembly: RegistrationFilter(
ExcludeNamespaces = new[] { "MyApp.Internal" },
ExcludePatterns = new[] { "*Mock*", "*Test*" })]
// β Avoid: Overly broad patterns
[assembly: RegistrationFilter(ExcludePatterns = new[] { "*" })] // Excludes everything!
// β Avoid: Filters as primary registration control
// Instead of filtering, just don't add [Registration] attributeRuntime filtering allows you to exclude specific services when calling the registration methods, rather than at compile time. This is extremely useful when:
- Different applications need different subsets of services from a shared library
- You want to exclude services conditionally based on runtime configuration
- Testing scenarios require specific services to be excluded
- Multiple applications share the same domain library but have different infrastructure needs
All generated AddDependencyRegistrationsFrom*() methods support three optional filter parameters:
services.AddDependencyRegistrationsFromDomain(
excludedNamespaces: new[] { "MyApp.Domain.Internal" },
excludedPatterns: new[] { "*Test*", "*Mock*" },
excludedTypes: new[] { typeof(EmailService), typeof(SmsService) });Exclude specific types explicitly:
// Exclude specific services by type
services.AddDependencyRegistrationsFromDomain(
excludedTypes: new[] { typeof(EmailService), typeof(NotificationService) });Example Scenario: PetStore.Api uses email services, but PetStore.WpfApp doesn't need them:
// PetStore.Api - includes all services
services.AddDependencyRegistrationsFromDomain();
// PetStore.WpfApp - excludes email/notification services
services.AddDependencyRegistrationsFromDomain(
excludedTypes: new[] { typeof(EmailService), typeof(INotificationService) });Exclude entire namespaces (including sub-namespaces):
// Exclude all services in the Internal namespace
services.AddDependencyRegistrationsFromDomain(
excludedNamespaces: new[] { "MyApp.Domain.Internal" });
// Also excludes MyApp.Domain.Internal.Utils, MyApp.Domain.Internal.Deep, etc.Example Scenario: Different deployment environments need different services:
#if PRODUCTION
// Production: exclude development/debug services
services.AddDependencyRegistrationsFromDomain(
excludedNamespaces: new[] { "MyApp.Domain.Development", "MyApp.Domain.Debug" });
#else
// Development: include all services
services.AddDependencyRegistrationsFromDomain();
#endifExclude services using wildcard patterns (* = any characters, ? = single character):
// Exclude all mock, test, and fake services
services.AddDependencyRegistrationsFromDomain(
excludedPatterns: new[] { "*Mock*", "*Test*", "*Fake*", "*Stub*" });
// Exclude all services ending with "Helper" or "Utility"
services.AddDependencyRegistrationsFromDomain(
excludedPatterns: new[] { "*Helper", "*Utility" });Example Scenario: Exclude all logging services in a minimal deployment:
// Minimal deployment - no logging services
services.AddDependencyRegistrationsFromDomain(
excludedPatterns: new[] { "*Logger*", "*Logging*", "*Log*" });All three filter types can be used together:
services.AddDependencyRegistrationsFromDomain(
excludedNamespaces: new[] { "MyApp.Domain.Internal" },
excludedPatterns: new[] { "*Test*", "*Development*" },
excludedTypes: new[] { typeof(LegacyService), typeof(DeprecatedFeature) });Runtime filters are automatically propagated to referenced assemblies:
// Filters apply to Domain AND all referenced assemblies (DataAccess, Infrastructure, etc.)
services.AddDependencyRegistrationsFromDomain(
includeReferencedAssemblies: true,
excludedNamespaces: new[] { "*.Internal" },
excludedPatterns: new[] { "*Test*" },
excludedTypes: new[] { typeof(EmailService) });All referenced assemblies will also exclude:
- Any namespace ending with
.Internal - Any type matching
*Test*pattern - The
EmailServicetype
| Feature | Compile-Time (Assembly) | Runtime (Method Parameters) |
|---|---|---|
| Applied When | During source generation | During service registration |
| Scope | All registrations from assembly | Specific registration call |
| Use Case | Global exclusions (test/mock services) | Application-specific exclusions |
| Configured In | AssemblyInfo.cs with [RegistrationFilter] |
Method call parameters |
| Flexibility | Fixed at compile time | Can vary per application/scenario |
Shared Domain Library (PetStore.Domain):
namespace PetStore.Domain.Services;
[Registration]
public class PetService : IPetService { } // Core service - needed by all apps
[Registration]
public class EmailService : IEmailService { } // Email - only needed by API
[Registration]
public class ReportService : IReportService { } // Reports - only needed by Admin
[Registration]
public class NotificationService : INotificationService { } // Notifications - only APIPetStore.Api (Web API - needs email + notifications):
// Include all services
services.AddDependencyRegistrationsFromDomain();PetStore.WpfApp (Desktop app - needs reports, not email):
// Exclude email and notification services
services.AddDependencyRegistrationsFromDomain(
excludedTypes: new[] { typeof(EmailService), typeof(NotificationService) });PetStore.AdminPortal (Admin - needs reports, not notifications):
// Exclude notification services
services.AddDependencyRegistrationsFromDomain(
excludedTypes: new[] { typeof(NotificationService) });PetStore.MobileApp (Minimal deployment):
// Exclude email, notifications, and reports
services.AddDependencyRegistrationsFromDomain(
excludedTypes: new[]
{
typeof(EmailService),
typeof(NotificationService),
typeof(ReportService)
});β Do:
- Use runtime filtering when different applications need different service subsets
- Use type exclusion for specific services you know by name
- Use pattern exclusion for groups of services (e.g., all
*Mock*services) - Use namespace exclusion for entire feature areas
- Combine with compile-time filters for maximum control
// Good: Application-specific exclusions
services.AddDependencyRegistrationsFromDomain(
excludedTypes: new[] { typeof(EmailService) }); // This app doesn't send emailsβ Avoid:
- Using overly broad patterns that might accidentally exclude needed services
- Runtime filtering as a replacement for proper service design
- Filtering when you should just not add
[Registration]attribute
// Bad: Overly broad pattern
services.AddDependencyRegistrationsFromDomain(
excludedPatterns: new[] { "*Service*" }); // Excludes almost everything!
// Bad: Should use compile-time filtering instead
services.AddDependencyRegistrationsFromDomain(
excludedPatterns: new[] { "*Test*" }); // Tests should be excluded at compile timeYou can verify which services are excluded by inspecting the service collection:
services.AddDependencyRegistrationsFromDomain(
excludedTypes: new[] { typeof(EmailService) });
// Verify EmailService is not registered
var emailService = serviceProvider.GetService<IEmailService>();
Console.WriteLine($"EmailService registered: {emailService != null}"); // FalseThe decorator pattern allows you to wrap existing services with additional functionality (logging, caching, validation, etc.) without modifying the original implementation. This is perfect for implementing cross-cutting concerns in a clean, maintainable way.
- Register the base service normally with
[Registration] - Create a decorator class that implements the same interface and wraps the base service
- Mark the decorator with
[Registration(Decorator = true)] - The generator automatically:
- Registers base services first
- Then registers decorators that wrap them
- Preserves the service lifetime
// Interface
public interface IOrderService
{
Task PlaceOrderAsync(string orderId);
}
// Base service - registered first
[Registration(Lifetime.Scoped, As = typeof(IOrderService))]
public class OrderService : IOrderService
{
public Task PlaceOrderAsync(string orderId)
{
Console.WriteLine($"[OrderService] Processing order {orderId}");
return Task.CompletedTask;
}
}
// Decorator - wraps the base service
[Registration(Lifetime.Scoped, As = typeof(IOrderService), Decorator = true)]
public class LoggingOrderServiceDecorator : IOrderService
{
private readonly IOrderService inner;
private readonly ILogger<LoggingOrderServiceDecorator> logger;
// First parameter MUST be the interface being decorated
public LoggingOrderServiceDecorator(
IOrderService inner,
ILogger<LoggingOrderServiceDecorator> logger)
{
this.inner = inner;
this.logger = logger;
}
public async Task PlaceOrderAsync(string orderId)
{
logger.LogInformation("Before placing order {OrderId}", orderId);
await inner.PlaceOrderAsync(orderId);
logger.LogInformation("After placing order {OrderId}", orderId);
}
}Usage:
services.AddDependencyRegistrationsFromDomain();
var orderService = serviceProvider.GetRequiredService<IOrderService>();
await orderService.PlaceOrderAsync("ORDER-123");
// Output:
// [LoggingDecorator] Before placing order ORDER-123
// [OrderService] Processing order ORDER-123
// [LoggingDecorator] After placing order ORDER-123The generator creates special Decorate extension methods that:
- Find the existing service registration
- Remove it from the service collection
- Create a new registration that resolves the original and wraps it
// Generated registration code
services.AddScoped<IOrderService, OrderService>(); // Base service
services.Decorate<IOrderService>((provider, inner) => // Decorator
{
return ActivatorUtilities.CreateInstance<LoggingOrderServiceDecorator>(provider, inner);
});You can stack multiple decorators - they are applied in the order they are discovered:
[Registration(Lifetime.Scoped, As = typeof(IOrderService))]
public class OrderService : IOrderService { }
[Registration(Lifetime.Scoped, As = typeof(IOrderService), Decorator = true)]
public class LoggingDecorator : IOrderService { }
[Registration(Lifetime.Scoped, As = typeof(IOrderService), Decorator = true)]
public class ValidationDecorator : IOrderService { }
[Registration(Lifetime.Scoped, As = typeof(IOrderService), Decorator = true)]
public class CachingDecorator : IOrderService { }Result: CachingDecorator β ValidationDecorator β LoggingDecorator β OrderService
[Registration(Decorator = true)]
public class AuditingDecorator : IPetService
{
private readonly IPetService inner;
private readonly IAuditLog auditLog;
public Pet CreatePet(CreatePetRequest request)
{
var result = inner.CreatePet(request);
auditLog.Log($"Created pet {result.Id} by {currentUser}");
return result;
}
}[Registration(Decorator = true)]
public class CachingPetServiceDecorator : IPetService
{
private readonly IPetService inner;
private readonly IMemoryCache cache;
public Pet? GetById(Guid id)
{
return cache.GetOrCreate($"pet:{id}", entry =>
{
entry.SlidingExpiration = TimeSpan.FromMinutes(5);
return inner.GetById(id);
});
}
}[Registration(Decorator = true)]
public class ValidationDecorator : IPetService
{
private readonly IPetService inner;
private readonly IValidator<CreatePetRequest> validator;
public Pet CreatePet(CreatePetRequest request)
{
var validationResult = validator.Validate(request);
if (!validationResult.IsValid)
{
throw new ValidationException(validationResult.Errors);
}
return inner.CreatePet(request);
}
}[Registration(Decorator = true)]
public class RetryDecorator : IExternalApiService
{
private readonly IExternalApiService inner;
public async Task<Result> CallApiAsync()
{
for (int i = 0; i < 3; i++)
{
try
{
return await inner.CallApiAsync();
}
catch (HttpRequestException) when (i < 2)
{
await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, i)));
}
}
}
}-
Explicit
AsRequired: Decorators MUST specify theAsparameter to indicate which interface they decorate// β Won't work - missing As parameter [Registration(Decorator = true)] public class MyDecorator : IService { } // β Correct [Registration(As = typeof(IService), Decorator = true)] public class MyDecorator : IService { }
-
Constructor First Parameter: The decorator's constructor must accept the interface as the first parameter
// β Correct - interface is first parameter public MyDecorator(IService inner, ILogger logger) { } // β Also correct - only parameter public MyDecorator(IService inner) { }
-
Matching Lifetime: Decorators inherit the lifetime of the base service registration
-
Registration Order: Base services are always registered before decorators, regardless of file order
See the PetStore.Domain sample for a complete working example:
- Base service: PetService.cs
- Decorator: LoggingPetServiceDecorator.cs
Conditional Registration allows you to register services based on configuration values at runtime. This is perfect for feature flags, environment-specific services, A/B testing, and gradual rollouts.
Services with a Condition parameter are only registered if the configuration value at the specified key path evaluates to true. The condition is checked at runtime when the registration methods are called.
When an assembly contains services with conditional registration:
- An
IConfigurationparameter is automatically added to all generated extension method signatures - The configuration value is checked using
configuration.GetValue<bool>("key") - Services are registered inside
ifblocks based on the condition
// Register RedisCache only when Features:UseRedisCache is true
[Registration(As = typeof(ICache), Condition = "Features:UseRedisCache")]
public class RedisCache : ICache
{
public string Get(string key) => /* Redis implementation */;
public void Set(string key, string value) => /* Redis implementation */;
}
// Register MemoryCache only when Features:UseRedisCache is false
[Registration(As = typeof(ICache), Condition = "!Features:UseRedisCache")]
public class MemoryCache : ICache
{
public string Get(string key) => /* In-memory implementation */;
public void Set(string key, string value) => /* In-memory implementation */;
}Configuration (appsettings.json):
{
"Features": {
"UseRedisCache": true
}
}Usage:
var configuration = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.Build();
// IConfiguration is required when conditional services exist
services.AddDependencyRegistrationsFromDomain(configuration);
// Resolves to RedisCache (because Features:UseRedisCache = true)
var cache = serviceProvider.GetRequiredService<ICache>();public static IServiceCollection AddDependencyRegistrationsFromDomain(
this IServiceCollection services,
IConfiguration configuration)
{
// Conditional registration with positive check
if (configuration.GetValue<bool>("Features:UseRedisCache"))
{
services.AddSingleton<ICache, RedisCache>();
}
// Conditional registration with negation
if (!configuration.GetValue<bool>("Features:UseRedisCache"))
{
services.AddSingleton<ICache, MemoryCache>();
}
return services;
}Prefix the condition with ! to negate it (register when the value is false):
// Register when Features:UseRedisCache is FALSE
[Registration(As = typeof(ICache), Condition = "!Features:UseRedisCache")]
public class MemoryCache : ICache { }Generated:
if (!configuration.GetValue<bool>("Features:UseRedisCache"))
{
services.AddSingleton<ICache, MemoryCache>();
}Enable/disable features without code changes:
// Premium features only when enabled
[Registration(Lifetime.Scoped, As = typeof(IPremiumService), Condition = "Features:EnablePremium")]
public class PremiumService : IPremiumService
{
public void ExecutePremiumFeature() { /* Premium logic */ }
}Configuration:
{
"Features": {
"EnablePremium": true
}
}Different implementations for different environments:
// Production email service
[Registration(As = typeof(IEmailService), Condition = "Environment:IsProduction")]
public class SendGridEmailService : IEmailService { }
// Development email service (logs instead of sending)
[Registration(As = typeof(IEmailService), Condition = "!Environment:IsProduction")]
public class LoggingEmailService : IEmailService { }Register different implementations based on experiment configuration:
[Registration(As = typeof(IRecommendationEngine), Condition = "Experiments:UseNewAlgorithm")]
public class NewRecommendationEngine : IRecommendationEngine { }
[Registration(As = typeof(IRecommendationEngine), Condition = "!Experiments:UseNewAlgorithm")]
public class LegacyRecommendationEngine : IRecommendationEngine { }Disable expensive services when not needed:
// AI service only when enabled (cost-saving)
[Registration(As = typeof(IAIService), Condition = "Services:EnableAI")]
public class OpenAIService : IAIService { }
// Fallback simple service
[Registration(As = typeof(IAIService), Condition = "!Services:EnableAI")]
public class BasicTextService : IAIService { }[Registration(As = typeof(IStorage), Condition = "Storage:UseAzure")]
public class AzureBlobStorage : IStorage { }
[Registration(As = typeof(IStorage), Condition = "Storage:UseAWS")]
public class S3Storage : IStorage { }
[Registration(As = typeof(IStorage), Condition = "Storage:UseLocal")]
public class LocalFileStorage : IStorage { }Configuration:
{
"Storage": {
"UseAzure": false,
"UseAWS": true,
"UseLocal": false
}
}// Scoped Redis cache (production)
[Registration(Lifetime.Scoped, As = typeof(ICache), Condition = "Cache:UseRedis")]
public class RedisCache : ICache { }
// Singleton memory cache (development)
[Registration(Lifetime.Singleton, As = typeof(ICache), Condition = "!Cache:UseRedis")]
public class MemoryCache : ICache { }// Always registered (core service)
[Registration(Lifetime.Scoped, As = typeof(IUserService))]
public class UserService : IUserService { }
// Conditionally registered (optional feature)
[Registration(Lifetime.Scoped, As = typeof(IAnalyticsService), Condition = "Features:EnableAnalytics")]
public class AnalyticsService : IAnalyticsService { }1. Use Hierarchical Configuration Keys:
// Good: Organized hierarchy
[Registration(Condition = "Features:Cache:UseRedis")]
[Registration(Condition = "Features:Email:UseSendGrid")]
[Registration(Condition = "Experiments:NewUI:Enabled")]2. Boolean Values:
Conditions always use GetValue<bool>(), so ensure configuration values are boolean:
{
"Features": {
"UseRedisCache": true, // β
Correct
"EnablePremium": "true", // β οΈ Works but not ideal
"UseNewAlgorithm": 1 // β Won't work as expected
}
}3. Default Values:
If a configuration key is missing, GetValue<bool>() returns false by default:
// If "Features:UseRedisCache" doesn't exist in config, MemoryCache is used
[Registration(Condition = "Features:UseRedisCache")]
public class RedisCache : ICache { }
[Registration(Condition = "!Features:UseRedisCache")]
public class MemoryCache : ICache { } // β This one is registered (default false)Without Conditional Services:
// No conditional services β no IConfiguration parameter
services.AddDependencyRegistrationsFromDomain();With Conditional Services:
// Has conditional services β IConfiguration parameter required
services.AddDependencyRegistrationsFromDomain(configuration);All Overloads Updated:
When conditional services exist, ALL generated overloads include the IConfiguration parameter:
// Overload 1: Basic registration
services.AddDependencyRegistrationsFromDomain(configuration);
// Overload 2: With transitive registration
services.AddDependencyRegistrationsFromDomain(configuration, includeReferencedAssemblies: true);
// Overload 3: With specific assembly
services.AddDependencyRegistrationsFromDomain(configuration, "DataAccess");
// Overload 4: With multiple assemblies
services.AddDependencyRegistrationsFromDomain(configuration, "DataAccess", "Infrastructure");1. Configuration Not Passed Transitively:
Configuration is NOT passed to referenced assemblies automatically. Each assembly manages its own conditional services:
// Domain has conditional services β needs configuration
services.AddDependencyRegistrationsFromDomain(configuration);
// DataAccess also has conditional services β call it directly with configuration
services.AddDependencyRegistrationsFromDataAccess(configuration);
// Or use transitive registration (but configuration isn't passed through)
services.AddDependencyRegistrationsFromDomain(configuration, includeReferencedAssemblies: true);
// β This registers DataAccess services, but they won't have conditional logic applied2. Thread-Safe:
Configuration reading is thread-safe and can be used in concurrent scenarios.
3. Native AOT Compatible:
Conditional registration is fully compatible with Native AOT since all checks are simple boolean reads from configuration.
4. No Circular Dependencies:
Be careful not to create circular dependencies between conditional services.
- π― Feature Flags: Enable/disable features dynamically without redeployment
- π Environment-Specific: Different implementations for dev/staging/prod
- π§ͺ A/B Testing: Easy experimentation with different implementations
- π° Cost Optimization: Disable expensive services when not needed
- π Gradual Rollout: Safely test new implementations before full deployment
- π¨ Clean Code: No
#ifpreprocessor directives needed - β‘ Runtime Flexibility: Change service implementations via configuration
- π Type-Safe: All registrations validated at compile time
See the Atc.SourceGenerators.DependencyRegistration sample for a complete working example:
- appsettings.json: Feature flag configuration
- RedisCache.cs: Conditional service (Features:UseRedisCache = true)
- MemoryCache.cs: Conditional service (Features:UseRedisCache = false)
- PremiumFeatureService.cs: Conditional premium features
- Program.cs: Usage demonstration
See the sample projects for complete working examples:
- Simple Sample: Atc.SourceGenerators.DependencyRegistration
- Domain Layer: Atc.SourceGenerators.DependencyRegistration.Domain