$npx -y skills add wshaddix/dotnet-skills --skill dotnet-csharp-dependency-injectionRegistering or resolving services with MS DI. Keyed services, scopes, decoration, hosted services.
| 1 | # dotnet-csharp-dependency-injection |
| 2 | |
| 3 | Advanced Microsoft.Extensions.DependencyInjection patterns for .NET applications. Covers service lifetimes, keyed services (net8.0+), decoration, factory delegates, scope validation, and hosted service registration. |
| 4 | |
| 5 | Cross-references: [skill:dotnet-csharp-async-patterns] for `BackgroundService` async patterns, [skill:dotnet-csharp-configuration] for `IOptions<T>` binding. |
| 6 | |
| 7 | --- |
| 8 | |
| 9 | ## Service Lifetimes |
| 10 | |
| 11 | | Lifetime | Registration | When to Use | |
| 12 | |----------|-------------|-------------| |
| 13 | | Transient | `AddTransient<T>()` | Lightweight, stateless services. New instance per injection. | |
| 14 | | Scoped | `AddScoped<T>()` | Per-request state (EF Core `DbContext`, unit of work). | |
| 15 | | Singleton | `AddSingleton<T>()` | Thread-safe, stateless, or shared state (caches, config). | |
| 16 | |
| 17 | ### Lifetime Mismatches (Captive Dependencies) |
| 18 | |
| 19 | Never inject a shorter-lived service into a longer-lived one: |
| 20 | |
| 21 | ```csharp |
| 22 | // WRONG -- scoped DbContext captured in singleton = same context for all requests |
| 23 | builder.Services.AddSingleton<OrderService>(); // singleton |
| 24 | builder.Services.AddScoped<AppDbContext>(); // scoped -- CAPTIVE! |
| 25 | |
| 26 | // CORRECT -- use IServiceScopeFactory in singletons |
| 27 | public sealed class OrderService(IServiceScopeFactory scopeFactory) |
| 28 | { |
| 29 | public async Task ProcessAsync(CancellationToken ct = default) |
| 30 | { |
| 31 | using var scope = scopeFactory.CreateScope(); |
| 32 | var db = scope.ServiceProvider.GetRequiredService<AppDbContext>(); |
| 33 | await db.Orders.Where(o => o.IsPending).ToListAsync(ct); |
| 34 | } |
| 35 | } |
| 36 | ``` |
| 37 | |
| 38 | ### Enable Scope Validation (Development) |
| 39 | |
| 40 | ```csharp |
| 41 | var builder = WebApplication.CreateBuilder(args); |
| 42 | // In Development, ValidateScopes is already true by default. |
| 43 | // For non-web hosts: |
| 44 | var host = Host.CreateDefaultBuilder(args) |
| 45 | .UseDefaultServiceProvider(options => |
| 46 | { |
| 47 | options.ValidateScopes = true; |
| 48 | options.ValidateOnBuild = true; // Validates all registrations at startup |
| 49 | }) |
| 50 | .Build(); |
| 51 | ``` |
| 52 | |
| 53 | --- |
| 54 | |
| 55 | ## Registration Patterns |
| 56 | |
| 57 | ### Interface-Implementation Pair |
| 58 | |
| 59 | ```csharp |
| 60 | builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>(); |
| 61 | ``` |
| 62 | |
| 63 | ### Multiple Implementations |
| 64 | |
| 65 | ```csharp |
| 66 | // Register multiple implementations |
| 67 | builder.Services.AddScoped<INotifier, EmailNotifier>(); |
| 68 | builder.Services.AddScoped<INotifier, SmsNotifier>(); |
| 69 | builder.Services.AddScoped<INotifier, PushNotifier>(); |
| 70 | |
| 71 | // Inject all -- order matches registration order |
| 72 | public sealed class CompositeNotifier(IEnumerable<INotifier> notifiers) |
| 73 | { |
| 74 | public async Task NotifyAsync(string message, CancellationToken ct = default) |
| 75 | { |
| 76 | foreach (var notifier in notifiers) |
| 77 | { |
| 78 | await notifier.NotifyAsync(message, ct); |
| 79 | } |
| 80 | } |
| 81 | } |
| 82 | ``` |
| 83 | |
| 84 | ### Factory Delegates |
| 85 | |
| 86 | ```csharp |
| 87 | builder.Services.AddScoped<IOrderService>(sp => |
| 88 | { |
| 89 | var repo = sp.GetRequiredService<IOrderRepository>(); |
| 90 | var logger = sp.GetRequiredService<ILogger<OrderService>>(); |
| 91 | var options = sp.GetRequiredService<IOptions<OrderOptions>>(); |
| 92 | return new OrderService(repo, logger, options.Value.MaxRetries); |
| 93 | }); |
| 94 | ``` |
| 95 | |
| 96 | ### `TryAdd` for Library Registrations |
| 97 | |
| 98 | Libraries should use `TryAdd` so applications can override: |
| 99 | |
| 100 | ```csharp |
| 101 | // Library code -- won't overwrite app registrations |
| 102 | builder.Services.TryAddScoped<IOrderRepository, DefaultOrderRepository>(); |
| 103 | |
| 104 | // Application code -- takes precedence if registered first |
| 105 | builder.Services.AddScoped<IOrderRepository, CustomOrderRepository>(); |
| 106 | ``` |
| 107 | |
| 108 | --- |
| 109 | |
| 110 | ## Keyed Services (net8.0+) |
| 111 | |
| 112 | Register and resolve services by a key, replacing the need for named service patterns. |
| 113 | |
| 114 | ```csharp |
| 115 | // Registration |
| 116 | builder.Services.AddKeyedScoped<ICache, RedisCache>("distributed"); |
| 117 | builder.Services.AddKeyedScoped<ICache, MemoryCache>("local"); |
| 118 | |
| 119 | // Injection via attribute |
| 120 | public sealed class OrderService( |
| 121 | [FromKeyedServices("distributed")] ICache distributedCache, |
| 122 | [FromKeyedServices("local")] ICache localCache) |
| 123 | { |
| 124 | public async Task<Order?> GetAsync(int id, CancellationToken ct = default) |
| 125 | { |
| 126 | // Check local cache first, then distributed |
| 127 | return await localCache.GetAsync<Order>(id.ToString(), ct) |
| 128 | ?? await distributedCache.GetAsync<Order>(id.ToString(), ct); |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | // Manual resolution |
| 133 | var cache = sp.GetRequiredKeyedService<ICache>("distributed"); |
| 134 | ``` |
| 135 | |
| 136 | > **net8.0+ only.** On earlier TFMs, use factory patterns or a dictionary-based approach. |
| 137 | |
| 138 | --- |
| 139 | |
| 140 | ## Decoration Pattern |
| 141 | |
| 142 | The built-in container does not natively support decoration. Use one of these approaches: |
| 143 | |
| 144 | ### Manual Decoration |
| 145 | |
| 146 | ```csharp |
| 147 | builder.Services.AddScoped<SqlOrderRepository>(); |
| 148 | builder.Services.AddScoped<IOrderRepository>(sp => |
| 149 | { |
| 150 | var inner = sp.GetRequiredService<SqlOrderRepository>(); |
| 151 | var logger = sp.GetRequiredService<ILogger<LoggingOrderRepository>>(); |
| 152 | return new LoggingOrderRepository(inner, logger); |
| 153 | }); |
| 154 | |
| 155 | public sealed class L |