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>();
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.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.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.