Advanced Registration — Factories, Open Generics and Decorators

The built-in DI container is more capable than the basic AddScoped<IFoo, Foo>() pattern suggests. Factory delegates give you full control over instantiation, open generic registration covers an entire family of types with one line, multiple registrations for the same interface enable the Composite and Strategy patterns, and the Decorator pattern adds cross-cutting concerns (logging, caching, retry) without modifying existing classes. These advanced patterns are used throughout production ASP.NET Core applications.

Factory Delegates

// ── Factory delegate — full control over instantiation ────────────────────
builder.Services.AddScoped<IEmailSender>(sp =>
{
    var config = sp.GetRequiredService<IConfiguration>();
    var env    = sp.GetRequiredService<IWebHostEnvironment>();

    // Different implementation based on environment
    return env.IsDevelopment()
        ? new InMemoryEmailSender()
        : new SmtpEmailSender(config.GetRequiredSection("Smtp").Get<SmtpOptions>()!);
});

// ── Conditional registration based on configuration ───────────────────────
if (builder.Configuration["FeatureFlags:UseRedisCache"] == "true")
    builder.Services.AddSingleton<ICacheService, RedisCacheService>();
else
    builder.Services.AddSingleton<ICacheService, MemoryCacheService>();
Note: When using a factory delegate (sp => new Foo(sp.GetRequired...)), the sp parameter is the IServiceProvider — use it to resolve other registered services. The factory runs with the correct scope: if you register a Scoped service with a factory, the factory runs once per request scope; if Singleton, it runs once per application lifetime. The factory gives you access to IConfiguration, IWebHostEnvironment, and any other service to make conditional instantiation decisions.
Tip: The Decorator pattern with the built-in DI container requires a small workaround — you must resolve the inner service and wrap it: services.AddScoped<IPostRepository>(sp => new CachedPostRepository(sp.GetRequiredService<EfPostRepository>(), sp.GetRequiredService<ICacheService>())). The Scrutor library adds a Decorate<IPostRepository, CachedPostRepository>() extension method that handles this more elegantly. For production codebases, Scrutor is the recommended approach for decoration.
Warning: When registering multiple implementations of the same interface for Strategy/Composite patterns, the DI container resolves IEnumerable<T> to give you all registered implementations. However, resolving a single T (without IEnumerable) returns only the LAST registered implementation. This is intentional but surprises developers who expect the first or a specific one. If you need a specific implementation by name or condition, use a factory delegate that selects the right one based on a key or condition.

Open Generic Registration

// ── Open generic — one registration covers all type arguments ─────────────
// Register IRepository<> once — covers IRepository<Post>, IRepository<User>, etc.
builder.Services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>));

// When code requests IRepository<Post>, DI creates EfRepository<Post>
// When code requests IRepository<User>, DI creates EfRepository<User>
// No individual registration needed per entity type!

public class PostService(IRepository<Post> postRepo, IRepository<User> userRepo)
{
    // Both automatically resolved by open generic registration
}

Multiple Implementations — Strategy and Composite

// ── Register multiple implementations of the same interface ───────────────
builder.Services.AddScoped<INotificationSender, EmailNotificationSender>();
builder.Services.AddScoped<INotificationSender, SmsNotificationSender>();
builder.Services.AddScoped<INotificationSender, PushNotificationSender>();

// ── Resolve all implementations as IEnumerable ────────────────────────────
public class NotificationDispatcher(IEnumerable<INotificationSender> senders)
{
    // senders contains all three: Email, SMS, Push
    public async Task SendToAllChannelsAsync(string message, CancellationToken ct)
    {
        foreach (var sender in senders)
            await sender.SendAsync(message, ct);
    }
}
// Register the dispatcher
builder.Services.AddScoped<NotificationDispatcher>();

Common Mistakes

Mistake 1 — Resolving a single instance when multiple are registered (gets last)

❌ Wrong — resolving INotificationSender as a single instance only returns the last registered (PushNotificationSender):

var sender = sp.GetRequiredService<INotificationSender>();  // only Push!

✅ Correct — inject IEnumerable<INotificationSender> to get all registrations.

Mistake 2 — Forgetting the concrete type registration when using the Decorator pattern

For the decorator factory pattern to work, the inner concrete type (e.g., EfPostRepository) must be separately registered so the factory can resolve it.

🧠 Test Yourself

You have 20 entity types and register services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)). How many lines of registration code does this require, and what does it enable?