15 questions found
What is dependency injection, and how does ASP.NET Core's built-in DI container support it out of the box?
Beginner
Dependency injection is a design pattern where a class receives its dependencies from an external source rather than creating them itself, promoting loose coupling and testability. ASP.NET Core includes a built-in DI container where you register service implementations against interfaces in Program.cs, 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(IEmailSender emailSender) {
// emailSender is automatically 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?
Testing ASP.NET Core Applications;Configuration & Options Pattern
What is the difference between Singleton, Scoped, and Transient service lifetimes, and how does Scoped specifically map to an HTTP request?
Intermediate
Singleton creates one instance for the entire application lifetime, shared across all requests. Scoped creates one instance per client HTTP request (a new DI scope is created at the start of each request and disposed at its end), 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 HTTP 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.
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?
Entity Framework Core & Data Access;Background Tasks & Hosted Services
What is the 'captive dependency' problem, and how does ASP.NET Core's DI container help catch it automatically?
Advanced
A captive dependency occurs when a longer-lived service (Singleton) depends on a shorter-lived service (Scoped), effectively 'capturing' that shorter-lived instance for the Singleton's entire lifetime -- potentially causing serious bugs like a captured DbContext being reused indefinitely across unrelated requests. ASP.NET Core's DI container detects this by default (in Development, via scope validation) and throws an InvalidOperationException at service resolution time rather than allowing the bug to silently occur.
builder.Services.AddSingleton<INotificationService, NotificationService>();
builder.Services.AddScoped<AppDbContext>();
// NotificationService depending on AppDbContext throws at resolution:
// "Cannot consume scoped service 'AppDbContext' from singleton 'INotificationService'"
// Fix: inject IServiceScopeFactory into the Singleton and create scopes on demand instead
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 the built-in scope validation would have caught immediately during local development.
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 Tasks & Hosted Services;Entity Framework Core & Data Access
How do you register multiple implementations of the same interface and resolve all of them together via IEnumerable<T>?
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 in registration order, useful for patterns like a chain of validators or multiple notification channels that should all run for a given event.
builder.Services.AddScoped<INotifier, EmailNotifier>();
builder.Services.AddScoped<INotifier, SmsNotifier>();
builder.Services.AddScoped<INotifier, PushNotifier>();
public class NotificationService(IEnumerable<INotifier> 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 do keyed services (added in .NET 8) provide a more explicit alternative to this pattern?
Filters;Configuration & Options Pattern
What are keyed services (introduced in .NET 8), and what problem do they solve compared to injecting IEnumerable<T> when you need one specific implementation?
Advanced
Keyed services let you register multiple implementations of the same interface under distinct string or object keys, resolved explicitly via [FromKeyedServices("key")] or GetRequiredKeyedService(key) -- solving the ambiguity of IEnumerable<T> injection (which gives you everything, requiring filtering) when you actually need one specific, named implementation deterministically, without a hand-rolled factory pattern or string-based service locator.
builder.Services.AddKeyedScoped<IPaymentProcessor, StripeProcessor>("stripe");
builder.Services.AddKeyedScoped<IPaymentProcessor, PayPalProcessor>("paypal");
public class CheckoutService([FromKeyedServices("stripe")] IPaymentProcessor 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 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 Pattern;Controllers vs Minimal APIs
How do you use IServiceScopeFactory to manually create a DI scope outside the normal per-request pipeline, such as inside a background service?
Intermediate
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) 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(IServiceScopeFactory scopeFactory) : BackgroundService {
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 cleaned up
}
Real-world example
A background report-generation service creates a manual DI scope for each report run, resolving 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 calling IServiceProvider.CreateScope() directly?
Background Tasks & Hosted Services;Entity Framework Core & Data Access
How does the DI container's disposal behavior work for IDisposable services registered as Transient, and why can this cause unexpected memory retention?
Advanced
The DI container automatically tracks and disposes any IDisposable/IAsyncDisposable service it created, calling Dispose when the owning scope ends -- notably, Transient services are still tracked and disposed by their containing scope, not immediately after use, meaning many short-lived Transient disposables resolved repeatedly within one long-lived request scope are all held alive until that scope ends, which can cause unexpected memory retention in hot loops.
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?
Diagnostics & Performance;Memory Management & Garbage Collection
How do you register a service using a factory delegate instead of a simple type mapping, when construction requires reading configuration or resolving other services conditionally?
Intermediate
AddScoped/AddSingleton/AddTransient overloads accept a factory delegate (IServiceProvider sp) => new MyService(...), letting you perform custom construction logic -- like reading a configuration value to conditionally choose an implementation -- 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 Pattern;Authentication
How does dependency injection integrate with minimal API endpoint handlers versus controller constructors?
Advanced
Controllers exclusively use constructor injection, resolving all dependencies once when the controller instance is created per-request. Minimal API endpoint handlers instead inject services directly as delegate parameters, automatically inferred as coming from DI (rather than route/body binding) for registered service types -- letting each individual endpoint declare exactly the specific dependencies it needs without a shared constructor.
// Controller: constructor injection
public class ProductsController(IProductService service) : ControllerBase {
[HttpGet] public IActionResult GetAll() => Ok(service.GetAll());
}
// Minimal API: parameter injection, inferred automatically
app.MapGet("/products", (IProductService service) => service.GetAll());
Real-world example
A minimal API's simple GET endpoint declares only IProductService as a dependency, while a more complex POST endpoint additionally declares ILogger, avoiding the controller pattern's tendency to inject dependencies into a shared constructor not every action uses.
Common follow-ups: Does parameter-based injection have any measurable performance difference from constructor injection?;How does explicit [FromServices] disambiguate a service parameter from a route/body parameter?
Controllers vs Minimal APIs;Configuration & Options Pattern
What is the difference between constructor injection and using IServiceProvider directly to resolve dependencies imperatively (service location)?
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 complicates unit testing.
// Preferred: constructor injection, dependencies explicit
public class OrderService(IEmailSender emailSender) { ... }
// 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 ASP.NET Core Applications;Filters