16 questions foundIOptions<T>, IOptionsSnapshot<T>, and IOptionsMonitor<T> are themselves services resolved through the DI container (registered automatically when you call services.AddOptions() or implicitly via Configure<T>()), demonstrating that the Options pattern is built entirely on top of the standard DI infrastructure rather than being a separate mechanism -- IOptions<T> is registered as Singleton internally, IOptionsSnapshot<T> as Scoped, explaining their respective behavior around configuration change visibility.
// Configure<T> implicitly registers the necessary IOptions<T> infrastructure
builder.Services.Configure<SmtpSettings>(builder.Configuration.GetSection("Smtp"));
// Under the hood, this makes IOptions<SmtpSettings>, IOptionsSnapshot<SmtpSettings>,
// and IOptionsMonitor<SmtpSettings> all resolvable via the same DI container
Real-world example Understanding that IOptionsSnapshot<T> is registered as Scoped explains precisely why it can't be safely injected into a Singleton service -- it's the exact same captive dependency problem as any other Scoped service being consumed by a Singleton.
Common follow-ups: Why does IOptionsMonitor<T> avoid the captive dependency problem despite being usable in Singletons?;How would you build a custom pattern following these same DI principles?
Configuration & Options;Background Services
ASP.NET Core's DI system is built around abstractions (IServiceCollection for registration, IServiceProvider for resolution) that third-party containers can replace entirely via a custom IServiceProviderFactory, letting you register services using the built-in IServiceCollection (for compatibility with framework and library registrations) while gaining access to advanced features the built-in container lacks, like multiple constructor resolution, property injection, or more sophisticated modules/scanning -- Autofac is the most common choice for teams needing these advanced capabilities.
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
builder.Host.ConfigureContainer<ContainerBuilder>(containerBuilder => {
containerBuilder.RegisterModule<MyAutofacModule>();
});
Real-world example A large enterprise application with complex conditional registration requirements (based on assembly scanning and convention-based module discovery) adopts Autofac specifically for its more powerful registration API, while still leveraging built-in framework services registered via standard IServiceCollection calls.
Common follow-ups: What features does Autofac provide that the built-in container lacks?;What's the performance trade-off of using a more feature-rich third-party container?
Dependency Injection;.NET CLI SDK & Project Structure (csproj)
Unit tests typically construct the service under test directly, passing mock or fake implementations of its dependencies (created via a mocking library like Moq or NSubstitute, or hand-written test doubles) directly to the constructor -- bypassing the DI container entirely, since unit tests should test the class's logic in isolation, not the wiring itself (that's what integration tests using WebApplicationFactory are for).
[Fact]
public async Task ProcessOrder_SendsConfirmationEmail() {
var mockEmailSender = new Mock<IEmailSender>();
var sut = new OrderService(mockEmailSender.Object); // direct construction, no DI container
await sut.ProcessOrderAsync(new Order());
mockEmailSender.Verify(e => e.SendAsync(It.IsAny<string>(), It.IsAny<string>()), Times.Once);
}
Real-world example A test suite for OrderService directly constructs it with a mocked IEmailSender and IPaymentGateway, verifying business logic behavior in milliseconds without any DI container overhead or database/network dependencies.
Common follow-ups: When would an integration test actually need to resolve services through the real DI container?;How do you test a service with many dependencies without excessive mock setup boilerplate?
Testing in .NET (xUnit Integration & Unit Testing);Dependency Injection
GetService<T>() returns null if the requested service type isn't registered in the container, requiring the caller to handle the null case explicitly. GetRequiredService<T>() instead throws an InvalidOperationException immediately with a clear message identifying the missing service, which is generally preferred in application code since a missing required dependency usually indicates a configuration bug that should fail loudly and immediately rather than propagate a confusing null reference later.
var logger = serviceProvider.GetService<ILogger<MyClass>>(); // returns null if not registered
if (logger == null) { /* handle missing case */ }
var requiredLogger = serviceProvider.GetRequiredService<ILogger<MyClass>>(); // throws clearly if missing
Real-world example A team standardizes on GetRequiredService throughout their startup code specifically so a missing service registration causes an immediate, clear startup failure instead of a null reference exception deep inside unrelated business logic hours later.
Common follow-ups: When is GetService's null-returning behavior actually useful over GetRequiredService?;How does constructor injection avoid needing either of these calls directly?
Dependency Injection;Diagnostics & Performance
ILogger<T> is automatically resolvable for any type T without explicit registration, since the logging infrastructure registers a generic ILogger<> open generic factory -- the T parameter becomes the logger's 'category name' (typically the full type name), letting log output and filtering be scoped precisely to the class that generated each log entry.
public class OrderService {
private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger) => _logger = logger;
public void Process() {
_logger.LogInformation("Processing order"); // logged with category 'OrderService'
}
}
Real-world example A production log filter configured to show only Warning+ level messages from OrderService (but Debug level from PaymentService) relies entirely on each class injecting its own correctly-typed ILogger<T>, which automatically tags every log entry with the right category.
Common follow-ups: How does the logging configuration system use these category names for filtering?;Why is ILogger<T> registered without explicit AddScoped/AddSingleton calls?
Logging;Dependency Injection
Dependency injection (DI) is a design pattern where a class receives the dependencies it needs from outside (typically via its constructor) rather than creating them itself -- this loosens coupling (a class depends on an interface, not a concrete implementation), makes unit testing far easier (a test can inject a mock/fake implementation instead of the real dependency), and centralizes how dependencies are wired together, so swapping an implementation later doesn't require touching every class that uses it.
// Tightly coupled: OrderService creates its own dependency directly
public class OrderService {
private PaymentService _paymentService = new(); // hardcoded, hard to test or swap
}
// Loosely coupled via constructor injection
public class OrderService {
private readonly IPaymentService _paymentService;
public OrderService(IPaymentService paymentService) => _paymentService = paymentService;
}
// ASP.NET Core registers the mapping once, centrally
builder.Services.AddScoped<IPaymentService, PaymentService>();
Real-world example A team writing unit tests for OrderService injects a fake IPaymentService that always returns success, testing OrderService's own logic in complete isolation without needing a real payment gateway connection during every test run.
Common follow-ups: What's the difference between constructor, property, and method injection?;How does ASP.NET Core's built-in DI container decide an object's lifetime (transient, scoped, singleton)?
Dependency Injection;Design Patterns in C#