Loading learning content...
As applications grow, explicit registration of every service becomes tedious and error-prone. A system with 500 services requires 500 registration statements—an invitation for mistakes and a maintenance burden that slows development.
Automatic registration patterns address this by discovering and registering services based on conventions—predictable rules that determine how types should be registered without explicit statements for each one.
The core insight is simple: if your codebase follows consistent patterns (naming conventions, namespace structures, marker interfaces), the container can infer registration requirements by inspecting assemblies at startup.
This page covers assembly scanning, convention-based registration, batch registration strategies, and the art of balancing automation with explicitness. You'll learn to dramatically reduce configuration boilerplate while maintaining clarity and avoiding the 'magic' that makes systems hard to understand.
Consider a growing application with explicitly registered services:
// Explicit registration - 500 lines like these
container.bind<IUserService>(TYPES.IUserService).to(UserService);
container.bind<IOrderService>(TYPES.IOrderService).to(OrderService);
container.bind<IPaymentService>(TYPES.IPaymentService).to(PaymentService);
container.bind<IInventoryService>(TYPES.IInventoryService).to(InventoryService);
container.bind<IShippingService>(TYPES.IShippingService).to(ShippingService);
// ... hundreds more
Problems with purely explicit registration at scale:
The Convention-over-Configuration principle:
If 90% of your registrations follow the same pattern (IXxxService → XxxService, scoped lifetime), encode that pattern once and let the system apply it automatically. Reserve explicit registration for the 10% that deviate.
Automate the common cases; explicitly configure the exceptions. If most services are scoped, singleton, or follow predictable patterns, convention handles them. Unusual services (decorators, factories, conditional logic) get explicit attention where the deviation is visible and documented.
Assembly scanning (or reflection-based discovery) is the foundation of automatic registration. At application startup, the container inspects compiled assemblies to find types matching specific criteria, then registers them according to defined rules.
The scanning process:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
// Assembly scanning with Scrutor (popular .NET library)// Demonstrates various scanning and registration patterns public static class ServiceRegistration{ public static IServiceCollection AddApplicationServices( this IServiceCollection services) { // PATTERN 1: Scan assembly containing a marker type services.Scan(scan => scan .FromAssemblyOf<UserService>() // Scan assembly containing UserService // Include only classes .AddClasses(classes => classes .Where(type => type.Name.EndsWith("Service"))) // Name convention // Register as their implemented interfaces .AsImplementedInterfaces() // With scoped lifetime .WithScopedLifetime() ); // PATTERN 2: Multiple assemblies with different conventions services.Scan(scan => scan // Scan multiple assemblies .FromAssemblies( typeof(UserRepository).Assembly, // Data layer typeof(UserService).Assembly, // Application layer typeof(EmailNotificationSender).Assembly // Infrastructure ) // Repositories - transient .AddClasses(classes => classes .Where(type => type.Name.EndsWith("Repository"))) .AsImplementedInterfaces() .WithScopedLifetime() // Handlers - transient .AddClasses(classes => classes .AssignableTo(typeof(IEventHandler<>))) // Marker interface .AsImplementedInterfaces() .WithTransientLifetime() // Validators - transient .AddClasses(classes => classes .AssignableTo(typeof(IValidator<>))) .AsImplementedInterfaces() .WithTransientLifetime() ); // PATTERN 3: Attribute-based filtering services.Scan(scan => scan .FromAssemblyOf<ApplicationMarker>() // Only classes with [Service] attribute .AddClasses(classes => classes .WithAttribute<ServiceAttribute>()) .AsImplementedInterfaces() .WithLifetimeFromAttribute() // Lifetime specified in attribute ); // PATTERN 4: Namespace-based conventions services.Scan(scan => scan .FromAssemblyOf<ApplicationMarker>() // Services namespace -> Scoped .AddClasses(classes => classes .InNamespaceOf<UserService>()) // MyApp.Services namespace .AsImplementedInterfaces() .WithScopedLifetime() // Repositories namespace -> Scoped .AddClasses(classes => classes .InNamespaces("MyApp.Data.Repositories")) .AsImplementedInterfaces() .WithScopedLifetime() // Singletons namespace -> Singleton .AddClasses(classes => classes .InNamespaces("MyApp.Infrastructure.Singletons")) .AsImplementedInterfaces() .WithSingletonLifetime() ); return services; }} // Custom attribute for fine-grained control[AttributeUsage(AttributeTargets.Class)]public class ServiceAttribute : Attribute{ public ServiceLifetime Lifetime { get; } public ServiceAttribute(ServiceLifetime lifetime = ServiceLifetime.Scoped) { Lifetime = lifetime; }} // Usage[Service(ServiceLifetime.Singleton)]public class ConfigurationService : IConfigurationService{ // Implementation} [Service(ServiceLifetime.Scoped)]public class OrderProcessingService : IOrderProcessingService{ // Implementation}Assembly scanning uses reflection, which has performance cost. For applications with thousands of types, scanning can add noticeable startup latency. Mitigate by: (1) scanning only specific assemblies, not all loaded assemblies, (2) caching scan results in development, (3) using source generators for compile-time discovery in performance-critical scenarios.
Effective conventions are predictable, discoverable, and self-documenting. Here are the most common patterns:
| Convention | Rule | Example | Lifetime |
|---|---|---|---|
| Naming Suffix | Classes ending in 'Service' | UserService → IUserService | Scoped |
| Naming Suffix | Classes ending in 'Repository' | OrderRepository → IOrderRepository | Scoped |
| Naming Suffix | Classes ending in 'Handler' | CreateUserHandler → IHandler<CreateUser> | Transient |
| Marker Interface | Implements ITransientService | EmailSender : ITransientService | Transient |
| Marker Interface | Implements IScopedService | UserContext : IScopedService | Scoped |
| Marker Interface | Implements ISingletonService | CacheManager : ISingletonService | Singleton |
| Namespace | In *.Services namespace | MyApp.Services.UserService | Scoped |
| Namespace | In *.Singletons namespace | MyApp.Infrastructure.Singletons.Config | Singleton |
| Attribute | Has [Transient] attribute | [Transient] class Validator | Transient |
| Generic Interface | Implements IEventHandler<T> | UserCreatedHandler : IEventHandler<UserCreated> | Transient |
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
// Comprehensive convention pattern implementation// Shows multiple strategies working together // PATTERN 1: Marker Interfaces for Lifetime// Clean, explicit, no magic strings public interface ITransientService { }public interface IScopedService { }public interface ISingletonService { } // Usage - lifetime is obvious from interfacepublic class EmailValidator : IEmailValidator, ITransientService{ public bool Validate(string email) => email.Contains("@");} public class UserContextProvider : IUserContextProvider, IScopedService{ public UserContext Current { get; private set; }} public class ApplicationConfiguration : IApplicationConfiguration, ISingletonService{ public string Environment { get; } public string Version { get; }} // Registrationservices.Scan(scan => scan .FromAssemblyOf<ApplicationMarker>() .AddClasses(c => c.AssignableTo<ITransientService>()) .AsImplementedInterfaces() .WithTransientLifetime() .AddClasses(c => c.AssignableTo<IScopedService>()) .AsImplementedInterfaces() .WithScopedLifetime() .AddClasses(c => c.AssignableTo<ISingletonService>()) .AsImplementedInterfaces() .WithSingletonLifetime()); // PATTERN 2: Generic Handler Registration// Register all implementations of open generic interfaces public interface ICommandHandler<TCommand, TResult>{ Task<TResult> HandleAsync(TCommand command, CancellationToken ct);} public interface IEventHandler<TEvent>{ Task HandleAsync(TEvent @event, CancellationToken ct);} public interface IQueryHandler<TQuery, TResult>{ Task<TResult> HandleAsync(TQuery query, CancellationToken ct);} // Implementationspublic class CreateUserCommandHandler : ICommandHandler<CreateUserCommand, UserDto>{ public async Task<UserDto> HandleAsync( CreateUserCommand command, CancellationToken ct) { // Implementation return new UserDto(); }} public class UserCreatedEventHandler : IEventHandler<UserCreatedEvent>{ public async Task HandleAsync(UserCreatedEvent @event, CancellationToken ct) { // Send welcome email, etc. }} // Registration - single scan registers ALL handlersservices.Scan(scan => scan .FromAssemblyOf<CreateUserCommandHandler>() // Command handlers .AddClasses(c => c.AssignableTo(typeof(ICommandHandler<,>))) .AsImplementedInterfaces() .WithScopedLifetime() // Event handlers .AddClasses(c => c.AssignableTo(typeof(IEventHandler<>))) .AsImplementedInterfaces() .WithTransientLifetime() // Query handlers .AddClasses(c => c.AssignableTo(typeof(IQueryHandler<,>))) .AsImplementedInterfaces() .WithScopedLifetime()); // PATTERN 3: Convention with Explicit Overrides// Scan with conventions, then override specific registrations public static IServiceCollection AddDomainServices( this IServiceCollection services, IConfiguration configuration){ // Step 1: Bulk registration via conventions services.Scan(scan => scan .FromAssemblyOf<DomainMarker>() .AddClasses(c => c.Where(t => t.Name.EndsWith("Service"))) .AsImplementedInterfaces() .WithScopedLifetime() ); // Step 2: Explicit overrides for special cases // These replace the convention-registered versions // PaymentService needs special factory with configuration services.AddScoped<IPaymentService>(sp => { var gateway = configuration["Payment:Gateway"]; var apiKey = configuration["Payment:ApiKey"]; return gateway switch { "stripe" => new StripePaymentService(apiKey), "paypal" => new PayPalPaymentService(apiKey), _ => throw new InvalidOperationException($"Unknown gateway: {gateway}") }; }); // CacheService needs singleton, not scoped services.AddSingleton<ICacheService, RedisCacheService>(); // LoggingService needs decorator chain services.AddScoped<ILoggingService>(sp => { var baseLogger = new ConsoleLoggingService(); var metricsLogger = new MetricsLoggingDecorator(baseLogger, sp.GetRequiredService<IMetrics>()); return new AsyncLoggingDecorator(metricsLogger); }); return services;}Beyond simple scanning, sophisticated registration strategies handle complex scenarios:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155
// Advanced batch registration strategies // STRATEGY 1: Decorator Chain Registration// Automatically wrap services with cross-cutting concerns public static class DecoratorRegistration{ public static IServiceCollection AddDecoratedServices( this IServiceCollection services) { // Step 1: Register base implementations services.Scan(scan => scan .FromAssemblyOf<UserRepository>() .AddClasses(c => c.Where(t => t.Name.EndsWith("Repository"))) .AsImplementedInterfaces() .WithScopedLifetime() ); // Step 2: Wrap all IRepository<T> with decorators services.Decorate(typeof(IRepository<>), typeof(LoggingRepositoryDecorator<>)); services.Decorate(typeof(IRepository<>), typeof(CachingRepositoryDecorator<>)); services.Decorate(typeof(IRepository<>), typeof(MetricsRepositoryDecorator<>)); // Result: Metrics -> Caching -> Logging -> Base // Every repository gets all three decorators automatically return services; }} // STRATEGY 2: Module-Based Batch Registration// Each feature module registers its own services public interface IServiceModule{ void Register(IServiceCollection services, IConfiguration configuration);} public class UserModule : IServiceModule{ public void Register(IServiceCollection services, IConfiguration configuration) { services.Scan(scan => scan .FromAssemblyOf<UserModule>() .AddClasses(c => c.InNamespaceOf<UserService>()) .AsImplementedInterfaces() .WithScopedLifetime() ); }} public class OrderModule : IServiceModule{ public void Register(IServiceCollection services, IConfiguration configuration) { services.Scan(scan => scan .FromAssemblyOf<OrderModule>() .AddClasses(c => c.InNamespaceOf<OrderService>()) .AsImplementedInterfaces() .WithScopedLifetime() ); // Module-specific singleton services.AddSingleton<IOrderNumberGenerator, SequentialOrderNumberGenerator>(); }} // Auto-discover and apply all modulespublic static class ModuleRegistration{ public static IServiceCollection AddAllModules( this IServiceCollection services, IConfiguration configuration, params Assembly[] assemblies) { var moduleTypes = assemblies .SelectMany(a => a.GetTypes()) .Where(t => typeof(IServiceModule).IsAssignableFrom(t)) .Where(t => t.IsClass && !t.IsAbstract); foreach (var moduleType in moduleTypes) { var module = (IServiceModule)Activator.CreateInstance(moduleType)!; module.Register(services, configuration); } return services; }} // Usageservices.AddAllModules( configuration, typeof(UserModule).Assembly, typeof(OrderModule).Assembly, typeof(PaymentModule).Assembly); // STRATEGY 3: Profile-Based Registration// Different registrations for different environments public interface IRegistrationProfile{ bool IsActive(IWebHostEnvironment environment); void Register(IServiceCollection services);} public class DevelopmentProfile : IRegistrationProfile{ public bool IsActive(IWebHostEnvironment env) => env.IsDevelopment(); public void Register(IServiceCollection services) { // Development-specific registrations services.AddSingleton<IEmailSender, FakeEmailSender>(); services.AddSingleton<IPaymentGateway, MockPaymentGateway>(); services.AddSingleton<ILogger, ColoredConsoleLogger>(); }} public class ProductionProfile : IRegistrationProfile{ public bool IsActive(IWebHostEnvironment env) => env.IsProduction(); public void Register(IServiceCollection services) { // Production-specific registrations services.AddSingleton<IEmailSender, SendGridEmailSender>(); services.AddSingleton<IPaymentGateway, StripePaymentGateway>(); services.AddSingleton<ILogger, CloudWatchLogger>(); }} public static class ProfileRegistration{ public static IServiceCollection AddProfiles( this IServiceCollection services, IWebHostEnvironment environment) { var profiles = typeof(ProfileRegistration).Assembly .GetTypes() .Where(t => typeof(IRegistrationProfile).IsAssignableFrom(t)) .Where(t => t.IsClass && !t.IsAbstract) .Select(t => (IRegistrationProfile)Activator.CreateInstance(t)!) .Where(p => p.IsActive(environment)); foreach (var profile in profiles) { profile.Register(services); } return services; }}When using batch decoration, the order of Decorate() calls determines the decorator chain order. The last decorator registered becomes the outermost wrapper. Plan your decorator order carefully—logging typically goes innermost (to log actual operations), while caching goes outer (to avoid logging cache hits).
Automatic registration introduces risk: if conventions change or types don't match expected patterns, services may not be registered—and you won't know until runtime. Validation at startup catches these issues immediately.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
// Comprehensive registration validation and diagnostics public static class ContainerValidation{ // Validate all expected services are registered public static void ValidateRegistrations( this IServiceProvider serviceProvider, params Type[] expectedServices) { var missing = new List<Type>(); var errors = new List<(Type, Exception)>(); foreach (var serviceType in expectedServices) { try { var service = serviceProvider.GetService(serviceType); if (service == null) { missing.Add(serviceType); } } catch (Exception ex) { errors.Add((serviceType, ex)); } } if (missing.Any() || errors.Any()) { var sb = new StringBuilder(); sb.AppendLine("Container validation failed:"); if (missing.Any()) { sb.AppendLine("Missing registrations:"); foreach (var type in missing) { sb.AppendLine($" - {type.FullName}"); } } if (errors.Any()) { sb.AppendLine("Resolution errors:"); foreach (var (type, ex) in errors) { sb.AppendLine($" - {type.FullName}: {ex.Message}"); } } throw new InvalidOperationException(sb.ToString()); } } // Validate all interfaces have implementations public static void ValidateNoMissingImplementations( this IServiceCollection services, params Assembly[] assemblies) { // Find all interfaces marked as services var serviceInterfaces = assemblies .SelectMany(a => a.GetTypes()) .Where(t => t.IsInterface) .Where(t => t.Name.StartsWith("I")) .Where(t => t.Name.EndsWith("Service") || t.Name.EndsWith("Repository") || t.Name.EndsWith("Handler")) .ToList(); var registeredServices = services .Select(sd => sd.ServiceType) .ToHashSet(); var unregistered = serviceInterfaces .Where(i => !registeredServices.Contains(i)) .ToList(); if (unregistered.Any()) { var message = "Unregistered service interfaces:\n" + string.Join("\n", unregistered.Select(t => $" - {t.FullName}")); throw new InvalidOperationException(message); } }} // Diagnostic output for debugging registration issuespublic static class RegistrationDiagnostics{ public static void PrintRegistrations( this IServiceCollection services, ILogger logger) { var grouped = services .GroupBy(sd => sd.Lifetime) .OrderBy(g => g.Key); foreach (var group in grouped) { logger.LogInformation("=== {Lifetime} Services ===", group.Key); foreach (var descriptor in group.OrderBy(d => d.ServiceType.Name)) { var implName = descriptor.ImplementationType?.Name ?? descriptor.ImplementationFactory?.Method.Name ?? "Instance"; logger.LogInformation( " {ServiceType} -> {Implementation}", descriptor.ServiceType.Name, implName ); } } } // Detect potential issues public static void DetectCommonIssues( this IServiceCollection services, ILogger logger) { // Issue 1: Singleton depending on Scoped var singletons = services .Where(sd => sd.Lifetime == ServiceLifetime.Singleton) .Select(sd => sd.ServiceType) .ToHashSet(); var scoped = services .Where(sd => sd.Lifetime == ServiceLifetime.Scoped) .Select(sd => sd.ServiceType) .ToHashSet(); // Would need constructor inspection for full analysis // This is simplified // Issue 2: Multiple registrations for same service var duplicates = services .GroupBy(sd => sd.ServiceType) .Where(g => g.Count() > 1) .Select(g => new { Type = g.Key, Implementations = g.Select(sd => sd.ImplementationType?.Name).ToList() }); foreach (var dup in duplicates) { logger.LogWarning( "Multiple registrations for {Type}: {Implementations}", dup.Type.Name, string.Join(", ", dup.Implementations) ); } }} // Usage at startupvar builder = WebApplication.CreateBuilder(args); // Configure services with scanningbuilder.Services.AddApplicationServices(); // Validate before building#if DEBUGbuilder.Services.PrintRegistrations(builder.Logging.CreateLogger("DI"));builder.Services.DetectCommonIssues(builder.Logging.CreateLogger("DI"));#endif var app = builder.Build(); // Validate critical services can be resolvedapp.Services.ValidateRegistrations( typeof(IUserService), typeof(IOrderService), typeof(IPaymentService), typeof(ILogger));The power of automatic registration comes with a cost: reduced visibility. When everything is auto-registered, it becomes harder to understand what's actually in the container and how services are configured.
Finding the right balance:
If understanding how a service is registered matters for debugging, make it explicit. If the registration is trivially predictable from conventions, automate it. When in doubt, explicit is better—you can always automate later, but understanding hidden registrations is painful.
Automatic registration dramatically reduces configuration boilerplate while maintaining system clarity. Here are the key principles:
What's Next:
The final page covers Container Best Practices—the collected wisdom for effectively using IoC containers: composition root design, handling cross-cutting concerns, avoiding common anti-patterns, and building maintainable container configurations at scale.
You now understand how to implement automatic registration using conventions, assembly scanning, and batch strategies. You can reduce configuration boilerplate while maintaining clarity, validate registrations at startup, and make principled decisions about what to automate versus keep explicit.