dependency-injection
codewithmukesh/dotnet-claude-kit
Dependency injection patterns for .NET 10: service lifetimes, keyed services, decorators, and common pitfalls.
What is dependency-injection?
Covers constructor injection, service lifetime management (Singleton, Scoped, Transient), keyed services for strategy patterns, decorator pattern for cross-cutting concerns, and factory patterns. Use this skill when registering services, resolving lifetime issues, designing service composition, or troubleshooting DI-related bugs like captive dependencies.
- Register and resolve services with correct lifetimes (Singleton, Scoped, Transient)
- Use keyed services (.NET 8+) to manage multiple implementations of the same interface
- Apply the decorator pattern for logging, validation, and cross-cutting concerns
- Implement factory patterns for runtime implementation selection
- Auto-register services by convention using Scrutor
- Bind configuration to strongly-typed options classes with validation
How to install dependency-injection
npx skills add https://github.com/codewithmukesh/dotnet-claude-kit --skill dependency-injectionHow to use dependency-injection
- 1.Identify your service's state and dependencies to determine the correct lifetime (Singleton for stateless config, Scoped for DbContext-dependent services, Transient for lightweight utilities)
- 2.Register services using AddScoped, AddSingleton, or AddTransient with interface-based registration (e.g., services.AddScoped<IOrderService, OrderService>())
- 3.For multiple implementations, use keyed services with AddKeyedScoped and resolve via [FromKeyedServices("key")] attribute or IServiceProvider.GetRequiredKeyedService()
- 4.Apply the decorator pattern by registering the base service, then using Scrutor's Decorate<TInterface, TDecorator>() to wrap it
- 5.Use IServiceScopeFactory in singletons to safely access scoped services without creating captive dependencies
- 6.For convention-based registration, use Scrutor's Scan() to auto-register classes matching marker interfaces
Use cases
- Setting up service registration in Program.cs with correct lifetimes to avoid memory leaks
- Selecting between multiple notification channels (email, SMS, push) using keyed services
- Adding logging or validation decorators to existing services without modifying core logic
- Configuring a payment processor based on runtime configuration (Stripe vs PayPal)
- Auto-registering all services in an assembly matching a marker interface
- Backend developers building .NET 10 applications
- Architects designing service composition and dependency graphs
- Teams adopting dependency injection for the first time
- Developers troubleshooting lifetime-related bugs and memory leaks
dependency-injection FAQ
A captive dependency occurs when a singleton service depends on a scoped or transient service. The scoped service gets captured in the singleton's lifetime, causing memory leaks and stale data. Fix it by injecting IServiceScopeFactory into the singleton and creating a new scope when needed.
Use keyed services when you need to select between multiple implementations at runtime (strategy pattern). Use decorators when you want to add behavior (logging, validation) to an existing service without changing its interface.
You can, but it's not recommended. Always register interfaces (e.g., AddScoped<IOrderService, OrderService>()) so you can swap implementations and test with mocks.
Use Scrutor's Scan() method with marker interfaces. Define IScopedService and ITransientService marker interfaces, implement them on your services, then use Scan() to register all classes matching those interfaces with the appropriate lifetime.
Scoped: one instance per HTTP request (default for DbContext). Singleton: one instance for the entire application lifetime (use for stateless config). Transient: new instance every time (rarely needed; use Scoped by default).
Full instructions (SKILL.md)
Source of truth, from codewithmukesh/dotnet-claude-kit.
name: dependency-injection description: > 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".
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
More from codewithmukesh/dotnet-claude-kit and the wider catalog.

docker
Multi-stage Docker builds and containerization patterns for .NET 10 applications.

dotnet-init
Interactively initialize .NET projects and generate customized CLAUDE.md files for Claude Code.

ef-core
Entity Framework Core patterns for .NET 10: DbContext, migrations, interceptors, compiled queries, and query optimization.

error-handling
Result pattern, ProblemDetails, and global exception handling for .NET 10 APIs

health-check
Multi-dimensional health assessment for .NET projects with letter grades (A-F) using Roslyn MCP tools.

httpclient-factory
IHttpClientFactory and resilience patterns for .NET 10 HTTP clients