Dependency Injection

16 questions found

What is dependency injection, and how does ASP.NET Core's built-in DI container support it?

Beginner
Dependency injection is a design pattern where a class receives its dependencies (other objects it needs to function) from an external source rather than creating them itself, promoting loose coupling and testability. ASP.NET Core includes a built-in DI container (IServiceProvider) where you register service implementations against interfaces at startup, and the framework automatically resolves and injects the correct implementation wherever a constructor requests that interface.
public interface IEmailSender { Task SendAsync(string to, string body); }
public class SmtpEmailSender : IEmailSender { ... }

builder.Services.AddScoped<IEmailSender, SmtpEmailSender>();

public class OrderService {
    private readonly IEmailSender _emailSender;
    public OrderService(IEmailSender emailSender) => _emailSender = emailSender;  // auto-injected
}
Real-world example A checkout service depends on IEmailSender rather than a concrete SmtpEmailSender, letting unit tests inject a fake implementation and letting the real application swap SMTP providers without changing OrderService's code at all.

Common follow-ups: What are the three built-in service lifetimes and how do they differ?;How does DI relate to the broader Inversion of Control principle?

ASP.NET Core Middleware & Request Pipeline;Testing in .NET (xUnit Integration & Unit Testing)

What is the difference between Singleton, Scoped, and Transient service lifetimes in ASP.NET Core's DI container?

Intermediate
Singleton creates one instance for the entire application lifetime, shared across all requests and threads. Scoped creates one instance per client request (or per explicitly created scope), shared within that single request but distinct across different requests. Transient creates a brand-new instance every single time the service is requested, even multiple times within the same request.
builder.Services.AddSingleton<ICacheService, MemoryCacheService>();  // one instance, app lifetime
builder.Services.AddScoped<AppDbContext>();                          // one instance per request
builder.Services.AddTransient<IEmailFormatter, EmailFormatter>();    // new instance every injection
Real-world example A DbContext is registered as Scoped so all repositories within a single HTTP request share the same context (and its change tracking), while a lightweight, stateless formatter is registered as Transient since creating a new instance each time is cheap and avoids any shared-state concerns.

Common follow-ups: What happens if a Singleton service depends on a Scoped service (captive dependency problem)?;How do you choose the right lifetime for a new service?

Configuration & Options;Entity Framework Core & Data Access

What is the 'captive dependency' problem, and how does the DI container help catch it?

Advanced
A captive dependency occurs when a longer-lived service (Singleton) depends on a shorter-lived service (Scoped or Transient), effectively 'capturing' that shorter-lived instance for the Singleton's entire lifetime -- defeating the purpose of the shorter lifetime and potentially causing serious bugs (like a captured DbContext being reused indefinitely across unrelated requests, sharing stale state or causing concurrency issues). ASP.NET Core's DI container detects this specific case (Singleton depending on Scoped) by default and throws an InvalidOperationException at service resolution time, unless scope validation is explicitly disabled.
builder.Services.AddSingleton<INotificationService, NotificationService>();
builder.Services.AddScoped<AppDbContext>();

// NotificationService depending on AppDbContext throws at startup/resolution:
// "Cannot consume scoped service 'AppDbContext' from singleton 'INotificationService'"

// Correct fix: inject IServiceScopeFactory into the Singleton instead
// and create scopes on demand for each unit of work
Real-world example A subtle production bug where all users started seeing the same cached database results was traced to a captive dependency -- a Singleton service had captured a Scoped DbContext, which validation would have caught immediately if it hadn't been running in an environment with scope validation disabled.

Common follow-ups: How do you fix a captive dependency once identified?;Why is scope validation enabled by default in Development but not always in Production?

Background Services;Entity Framework Core & Data Access

How do you register multiple implementations of the same interface, and how do you resolve all of them together?

Intermediate
Calling AddScoped/AddSingleton/AddTransient multiple times for the same interface with different implementations registers all of them; injecting IEnumerable<TInterface> resolves the full collection of registered implementations in registration order, useful for patterns like a chain of validators or multiple notification channels that should all run.
builder.Services.AddScoped<INotifier, EmailNotifier>();
builder.Services.AddScoped<INotifier, SmsNotifier>();
builder.Services.AddScoped<INotifier, PushNotifier>();

public class NotificationService {
    private readonly IEnumerable<INotifier> _notifiers;
    public NotificationService(IEnumerable<INotifier> notifiers) => _notifiers = notifiers;

    public async Task NotifyAllAsync(string message) {
        foreach (var notifier in _notifiers) await notifier.SendAsync(message);
    }
}
Real-world example An order confirmation feature registers Email, SMS, and Push notification implementations of INotifier, then injects IEnumerable<INotifier> to fan out a single order-confirmed event to every configured notification channel simultaneously.

Common follow-ups: How do you resolve just the LAST registered implementation instead of all of them?;How does keyed services (added in .NET 8) provide a more explicit alternative to this pattern?

Global Exception Handling & Middleware;Configuration & Options

What are keyed services (introduced in .NET 8), and what problem do they solve compared to multiple unnamed registrations?

Advanced
Keyed services let you register multiple implementations of the same interface under distinct string or object keys, then resolve a specific one explicitly using [FromKeyedServices("key")] or GetRequiredKeyedService(key) -- solving the ambiguity problem of IEnumerable<T> injection (which gives you everything, requiring you to filter or iterate) when you actually need one specific, named implementation deterministically, without resorting to a factory pattern or string-based service locator anti-pattern.
builder.Services.AddKeyedScoped<IPaymentProcessor, StripeProcessor>("stripe");
builder.Services.AddKeyedScoped<IPaymentProcessor, PayPalProcessor>("paypal");

public class CheckoutService {
    public CheckoutService([FromKeyedServices("stripe")] IPaymentProcessor processor) {
        _processor = processor;  // explicitly gets the Stripe implementation
    }
}
Real-world example A multi-payment-provider e-commerce platform uses keyed services to explicitly wire different checkout flows to specific payment processors (stripe, paypal, applepay) without needing a custom factory class or dictionary-based lookup that keyed services now handle natively.

Common follow-ups: How do keyed services interact with IEnumerable<T> injection -- can you still get all of them?;What's the migration path from a hand-rolled factory pattern to keyed services?

Configuration & Options;RESTful Web APIs & Controllers

How does the DI container handle constructor injection when a class has multiple constructors?

Intermediate
The default ASP.NET Core DI container requires exactly one constructor be resolvable (all its parameter types must be registered services) -- if multiple constructors could theoretically be satisfied, or if the class has multiple public constructors at all in some scenarios, the container throws an exception rather than guessing which one to use, since implicit 'pick the best constructor' behavior (common in other DI frameworks) is intentionally not supported by the built-in container to avoid ambiguity.
public class OrderService {
    // Having two constructors like this causes issues with the built-in container:
    public OrderService(IEmailSender emailSender) { ... }
    public OrderService(IEmailSender emailSender, ILogger<OrderService> logger) { ... }
    // Built-in container: ambiguous, throws at resolution time
}
// Fix: use exactly one constructor, or use a third-party container supporting this
Real-world example A team migrating from a third-party DI container (which supported multiple constructor overloads with fallback resolution) to the built-in ASP.NET Core container hits runtime errors until they consolidate each service down to a single constructor.

Common follow-ups: Which third-party containers (Autofac, etc.) do support multiple constructor resolution?;What's the recommended pattern instead of multiple constructors?

Dependency Injection;Diagnostics & Performance

How do you use IServiceScopeFactory to manually create and manage a DI scope outside the normal request pipeline?

Advanced
IServiceScopeFactory.CreateScope() returns an IServiceScope (implementing IDisposable) whose ServiceProvider can resolve scoped services within that isolated scope's lifetime -- essential for singleton or background contexts (like a BackgroundService or a Console app's Main method) that have no natural per-request scope but still need to consume scoped services like a DbContext, ensuring proper disposal via a using block once the unit of work completes.
public class ReportGenerator : BackgroundService {
    private readonly IServiceScopeFactory _scopeFactory;
    protected override async Task ExecuteAsync(CancellationToken ct) {
        using var scope = _scopeFactory.CreateScope();
        var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
        await GenerateReportAsync(db);
    }  // scope disposed here, DbContext and other scoped services cleaned up
}
Real-world example A console application's data migration tool creates a manual DI scope in its Main method to resolve and use scoped repository services exactly as they'd be used in a web request, despite having no actual HTTP request context.

Common follow-ups: What happens to scoped services if you never dispose the created scope?;How does this differ from using IServiceProvider.CreateScope() directly?

Background Services;Entity Framework Core & Data Access

What is the difference between constructor injection and service location (using IServiceProvider directly to resolve dependencies)?

Intermediate
Constructor injection explicitly declares a class's dependencies in its constructor signature, making them visible and enforced at compile time -- the preferred approach since it makes dependencies discoverable and testable. Service location instead has code call IServiceProvider.GetService<T>() imperatively wherever a dependency is needed, hiding the actual dependencies inside method bodies (an anti-pattern generally discouraged, since it obscures a class's true requirements and makes unit testing harder, though occasionally necessary for truly dynamic runtime resolution scenarios).
// Preferred: constructor injection, dependencies explicit
public class OrderService {
    public OrderService(IEmailSender emailSender) { ... }  // clear requirement
}

// Anti-pattern: service location, hidden dependency
public class OrderService {
    public void Process(IServiceProvider provider) {
        var emailSender = provider.GetService<IEmailSender>();  // hidden, hard to test
    }
}
Real-world example A code review rejects a pull request that resolves services via IServiceProvider.GetService<T>() inside a method body, requesting the dependency be moved to the constructor instead so it's visible in the class's public API and easily mockable in tests.

Common follow-ups: When is service location actually the pragmatic right choice (like plugin systems)?;How does this relate to the broader Service Locator anti-pattern debate?

Testing in .NET (xUnit Integration & Unit Testing);Dependency Injection

How does the DI container's disposal behavior work for IDisposable and IAsyncDisposable services across different lifetimes?

Advanced
The DI container automatically tracks and disposes any IDisposable/IAsyncDisposable service it created, calling Dispose/DisposeAsync when the owning scope ends (for Scoped/Transient services resolved within a request scope) or when the application shuts down (for Singleton services) -- notably, Transient services are still tracked and disposed by their containing scope, not immediately after use, which can cause unexpected memory retention if many short-lived Transient disposables accumulate within one long-lived scope.
public class ReportExporter : IDisposable {
    public void Dispose() => Console.WriteLine("Disposed");
}

builder.Services.AddTransient<ReportExporter>();
// Even though Transient, if resolved many times within one request scope,
// ALL instances are held and disposed together only when that request scope ends
Real-world example A memory profiling session reveals unexpectedly high memory retention within long request scopes traced to hundreds of Transient IDisposable service instances all being held alive until the scope's end rather than being freed immediately after each use.

Common follow-ups: Why doesn't the container dispose Transient services immediately after use?;How would you work around this for a Transient disposable used many times in a loop?

Memory Management & Garbage Collection;Diagnostics & Performance

How do you register a service using a factory delegate instead of a simple type mapping, when construction requires custom logic?

Intermediate
AddScoped/AddSingleton/AddTransient overloads accept a factory delegate `(IServiceProvider sp) => new MyService(...)` letting you perform custom construction logic -- like resolving other services manually, reading configuration values, or choosing an implementation conditionally -- that a simple type-to-type registration can't express.
builder.Services.AddScoped<IPaymentGateway>(sp => {
    var config = sp.GetRequiredService<IOptions<PaymentSettings>>().Value;
    return config.Provider switch {
        "stripe" => new StripeGateway(config.ApiKey),
        "paypal" => new PayPalGateway(config.ApiKey),
        _ => throw new InvalidOperationException("Unknown payment provider")
    };
});
Real-world example A payment processing service chooses its concrete gateway implementation dynamically based on a configuration value at registration time, using a factory delegate since the simple AddScoped<TInterface, TImplementation> overload can't express this conditional logic.

Common follow-ups: How does this compare to using keyed services for the same conditional-provider scenario?;What services can you safely resolve inside a factory delegate without lifetime issues?

Configuration & Options;Authentication & Authorization (Identity JWT OAuth)

Showing 1–10 of 16