Dependency Injection

SkillAI & models

This skill gives an AI working knowledge of dependency injection in .NET 10, so it can help you wire services into your applications the right way. Once added, your AI can guide you through registering services, choosing service lifetimes, and structuring code with decorators and factories while steering clear of common mistakes.

Available today. Use it from your connected AI after setup.

After adding this skill, ask your AI for help with a registration, a lifetime issue, or how to compose your services. It will also load automatically when a conversation mentions DI or dependency injection.

Then ask your AI: use the Dependency Injection skill

What your AI can do with it

  • Register services in .NET 10 applications
  • Choose the right service lifetime and fix lifetime problems
  • Use keyed services in your registrations
  • Structure code with the decorator and factory patterns
  • Design how services compose together
  • Spot and avoid common dependency injection pitfalls

What this skill tells your AI

The instructions your AI receives, as published by codewithmukesh/dotnet-claude-kit in skills/dependency-injection/SKILL.md and read by ahel’s review.

Core Principles

  1. Constructor injection is the default — Inject dependencies through the constructor (primary constructors make this clean). No service locator, no property injection.
  2. Match lifetimes carefully — A singleton must never depend on a scoped or transient service. This is the most common DI bug.
  3. Register interfaces, resolve interfaces — Register services.AddScoped<IOrderService, OrderService>(), not the concrete type.
  4. 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

ScenarioRecommendation
Stateless serviceScoped (default) or Transient
Configuration / cacheSingleton
DbContextScoped (registered by AddDbContext)
Multiple implementationsKeyed services (strategy pattern)
Cross-cutting behaviorDecorator pattern
Convention-based registrationScrutor
Runtime implementation selectionFactory delegate
Audit existing registrationsget_di_registrations MCP tool — lifetimes, duplicates, captive-dependency risks in one call
Strongly-typed configAddOptions<T>().BindConfiguration()

Signals

GitHub stars
710
Forks
167
Last commit
Aug 2026
Advanced
Catalog kind
skill
Gateway key
dependency-injection-codewithmukesh
Source
github.com/codewithmukesh/dotnet-claude-kit