dependency-injection
Dependency injection patterns for .NET 10. Covers service lifetimes, keyed services, the decorator pattern, factory pattern, and common DI pitfalls. Load this skill when registering services, resolving lifetime issues, designing service composition, or when the user mentions "DI", "dependency injection", "service registration", "AddScoped", "AddTransient", "AddSingleton", "keyed services", "decorator", "Scrutor", "IServiceCollection", or "captive dependency".
Install
npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill dependency-injectionSKILL.md
Dependency Injection
Core Principles
- Constructor injection is the default — Inject dependencies through the constructor (primary constructors make this clean). No service locator, no property injection.
- Match lifetimes carefully — A singleton must never depend on a scoped or transient service. This is the most common DI bug.
- Register interfaces, resolve interfaces — Register
services.AddScoped<IOrderService, OrderService>(), not the concrete type. - Keyed services for strategy pattern — .NET 8+ keyed services replace manual factory patterns for selecting between implementations.
Patterns
Keyed Services (.NET 8+)
Use keyed services to register and resolve multiple implementations of the same interface.
// Registration
builder.Services.AddKeyedScoped<INotificationService, EmailNotificationService>("email");
builder.Services.AddKeyedScoped<INotificationService, SmsNotificationService>("sms");
builder.Services.AddKeyedScoped<INotificationService, PushNotificationService>("push");
// Resolution via attribute
public class OrderHandler([FromKeyedServices("email")] INotificationService notifier)
{
public async Task Handle(CreateOrder.Command command, CancellationToken ct)
{
// ... create order
await notifier.SendAsync(notification, ct);
}
}
// Resolution via IServiceProvider
public class NotificationRouter(IServiceProvider provider)
{
public INotificationService GetService(string channel)
{
return provider.GetRequiredKeyedService<INotificationService>(channel);
}
}
Decorator Pattern
// Base service
public interface IOrderService
{
Task<Result<Order>> CreateAsync(CreateOrderRequest request, CancellationToken ct);
}
public class OrderService(AppDbContext db, TimeProvider clock) : IOrderService
{
public async Task<Result<Order>> CreateAsync(CreateOrderRequest request, CancellationToken ct)
{
var order = Order.Create(request, clock.GetUtcNow());
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
return Result.Success(order);
}
}
// Decorator — adds logging
public class LoggingOrderService(IOrderService inner, ILogger<LoggingOrderService> logger) : IOrderService
{
public async Task<Result<Order>> CreateAsync(CreateOrderRequest request, CancellationToken ct)
{
logger.LogInformation("Creating order for customer {CustomerId}", request.CustomerId);
var result = await inner.CreateAsync(request, ct);
if (result.IsSuccess)
logger.LogInformation("Order {OrderId} created", result.Value.Id);
return result;
}
}
// Registration with Scrutor
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.Decorate<IOrderService, LoggingOrderService>();
Registration by Convention (Scrutor)
// Auto-register all services matching a convention
builder.Services.Scan(scan => scan
.FromAssemblyOf<Program>()
.AddClasses(classes => classes.AssignableTo<ITransientService>())
.AsImplementedInterfaces()
.WithTransientLifetime()
.AddClasses(classes => classes.AssignableTo<IScopedService>())
.AsImplementedInterfaces()
.WithScopedLifetime());
Factory Pattern
When you need runtime logic to select an implementation.
builder.Services.AddScoped<IPaymentProcessor>(sp =>
{
var config = sp.GetRequiredService<IOptions<PaymentOptions>>().Value;
return config.Provider switch
{
"stripe" => ActivatorUtilities.CreateInstance<StripeProcessor>(sp),
"paypal" => ActivatorUtilities.CreateInstance<PayPalProcessor>(sp),
_ => throw new InvalidOperationException($"Unknown payment provider: {config.Provider}")
};
});
Options Registration
// Bind configuration section to a strongly-typed options class
builder.Services.AddOptions<JwtOptions>()
.BindConfiguration("Jwt")
.ValidateDataAnnotations()
.ValidateOnStart();
// Inject as IOptions<T>
public class TokenService(IOptions<JwtOptions> options)
{
private readonly JwtOptions _jwt = options.Value;
}
Anti-patterns
Don't Capture Scoped Services in Singletons
// BAD — DbContext is scoped, captured by singleton = memory leak + stale data
builder.Services.AddSingleton<OrderCache>(); // depends on AppDbContext
// GOOD — use IServiceScopeFactory in singleton
public class OrderCache(IServiceScopeFactory scopeFactory)
{
public async Task<Order?> GetAsync(Guid id)
{
await using var scope = scopeFactory.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await db.Orders.FindAsync(id);
}
}
Don't Register Everything as Singleton
// BAD — making a service singleton when it holds mutable state
builder.Services.AddSingleton<OrderService>(); // has DbContext dependency
// GOOD — match the lifetime to the service's needs
builder.Services.AddScoped<OrderService>();
Decision Guide
| Scenario | Recommendation |
|---|---|
| Stateless service | Scoped (default) or Transient |
| Configuration / cache | Singleton |
| DbContext | Scoped (registered by AddDbContext) |
| Multiple implementations | Keyed services (strategy pattern) |
| Cross-cutting behavior | Decorator pattern |
| Convention-based registration | Scrutor |
| Runtime implementation selection | Factory delegate |
| Audit existing registrations | get_di_registrations MCP tool — lifetimes, duplicates, captive-dependency risks in one call |
| Strongly-typed config | AddOptions<T>().BindConfiguration() |
Related skills
entra-app-registrationmicrosoft606KGuides Microsoft Entra ID app registration, OAuth 2.0 authentication, and MSAL integration. USE FOR: create app registration, register Azure AD app, configure OAuth, set up authentication, add API permissions, generate service principal, MSAL example, console app auth, Entra ID setup, Azure AD authentication. DO NOT USE FOR: Key Vault secrets (use azure-keyvault-expiration-audit), general Azure resource security guidance.azure-messagingmicrosoft595KTroubleshoot and resolve issues with Azure Messaging SDKs for Event Hubs and Service Bus. Covers connection failures, authentication errors, message processing issues, and SDK configuration problems. WHEN: event hub SDK error, service bus SDK issue, messaging connection failure, AMQP error, event processor host issue, message lock lost, message lock expired, lock renewal, lock renewal batch, send timeout, receiver disconnected, SDK troubleshooting, azure messaging SDK, event hub consumer, servicentra-agent-idmicrosoft328KProvision Microsoft Entra Agent Identity Blueprints, BlueprintPrincipals, and per-instance Agent Identities via Microsoft Graph, and configure OAuth 2.0 token exchange (fmi_path, OBO, cross-tenant) including the Microsoft Entra SDK for AgentID sidecar. USE FOR: Agent Identity Blueprint, BlueprintPrincipal, agent OAuth, fmi_path token exchange, agent OBO, Workload Identity Federation for agents, polyglot agent auth, Microsoft.Identity.Web.AgentIdentities. DO NOT USE FOR: standard Entra app registsupabasesupabase298KUse when doing ANY task involving Supabase. Triggers: Supabase products (Database, Auth, Edge Functions, Realtime, Storage, Vectors, Cron, Queues); client libraries and SSR integrations (supabase-js, @supabase/ssr) in Next.js, React, SvelteKit, Astro, Remix; auth issues (login, logout, sessions, JWT, cookies, getSession, getUser, getClaims, RLS); Supabase CLI or MCP server; schema changes, migrations, declarative schemas, security audits, Postgres extensions (pg_graphql, pg_cron, pg_vector); deb