ASP.NET Core Dependency Injection Interview Questions for Senior .NET Developers (2026)

When preparing for a senior .NET developer role, ASP.NET Core dependency injection interview questions are among the most frequently asked โ and most commonly mishandled โ topics at the mid-to-senior level. Interviewers aren't just looking for "Singleton, Scoped, Transient" recitation. They expect you to explain trade-offs, identify failure modes, and demonstrate you've used the DI container in production systems under real pressure.
๐ Want implementation-ready .NET source code you can drop straight into your project? Join Coding Droplets on Patreon for exclusive tutorials, premium code samples, and early access to new content. ๐ https://www.patreon.com/CodingDroplets
This guide covers the DI questions that actually appear in senior interviews โ from foundational concepts to advanced scenarios involving keyed services, custom containers, and lifetime pitfalls in ASP.NET Core 8 and 10.
Basic: Foundational DI Concepts
What Is Dependency Injection and Why Does ASP.NET Core Use It?
Dependency Injection (DI) is a design pattern where a class receives its dependencies from an external source rather than constructing them itself. ASP.NET Core's built-in DI container, IServiceCollection, implements this pattern as a first-class framework feature.
The core benefit is inversion of control: classes declare what they need (via constructor parameters), and the container wires them up. This makes unit testing straightforward, reduces coupling between components, and allows lifetime management to be centralised rather than scattered across the codebase.
Interviewers often follow up: "Could you build an ASP.NET Core app without DI?" The honest answer is yes โ but you'd be swimming against the framework. Middleware, controllers, Razor Pages, and the host startup pipeline all consume the DI container by default.
What Are the Three Built-In Service Lifetimes?
ASP.NET Core's DI container supports three lifetimes:
- Singleton โ one instance per application lifetime; created on first request, reused for every subsequent one
- Scoped โ one instance per HTTP request (or per logical scope); ideal for EF Core
DbContextand unit-of-work patterns - Transient โ a new instance every time the service is resolved; best for lightweight, stateless services
The important nuance interviewers probe: if you inject a Scoped service into a Singleton, you get a "captive dependency" โ the Scoped instance is effectively promoted to Singleton lifetime because it's held by the long-lived Singleton. In development builds, ASP.NET Core validates this and throws an InvalidOperationException. In production (scope validation is off by default unless explicitly enabled), it silently causes bugs.
What Is the Difference Between AddTransient, AddScoped, and AddSingleton?
These are the three registration extension methods on IServiceCollection. Beyond the lifetime difference:
AddSingletonalso has an overload that accepts a pre-constructed instance (services.AddSingleton<ICache>(myExistingCache)), which is useful for objects that pre-exist the container startupAddScopedis the right default for database contexts and anything involving per-request stateAddTransientis often overused; watch for transient services that hold unmanaged resources โ the container disposesIDisposabletransient instances only at scope disposal, not at resolution time, which can lead to resource exhaustion under high concurrency
Intermediate: Real-World DI Usage
How Does Constructor Injection Work in ASP.NET Core?
Constructor injection is the default and preferred pattern. The container inspects constructor parameters at resolution time and recursively resolves each dependency. If a required service is not registered, resolution fails with an InvalidOperationException.
A few practical points that come up in senior interviews:
- If a class has multiple constructors, the container picks the one with the largest number of satisfiable parameters (the "greedy" algorithm). This can cause unexpected behaviour when you add a new registered service that satisfies a previously unresolvable constructor.
- Property injection is not supported natively by the built-in container โ you need a third-party container like Autofac or a custom mechanism.
- Method injection (directly into action methods in controllers) is supported via
[FromServices], but it's an exception to the constructor injection rule rather than a pattern to encourage broadly.
What Is IServiceProvider and When Should You Avoid It?
IServiceProvider is the runtime interface for resolving services. You call serviceProvider.GetRequiredService<T>() to manually pull a dependency.
The problem: using IServiceProvider directly inside a class to resolve other services is the Service Locator anti-pattern. It hides dependencies (they aren't declared in the constructor), makes testing harder (you have to mock the provider, not the dependency), and bypasses lifetime validation.
Legitimate uses for direct IServiceProvider access:
- Inside a factory method that creates instances of types not known at registration time
- In
IHostedServiceimplementations that need to create a scope manually (since hosted services are singletons but may need scoped services) - Middleware that needs to resolve a scoped service at the start of a request pipeline
The correct pattern for hosted services:
IServiceScopeFactory โ CreateScope() โ scope.ServiceProvider.GetRequiredService<T>()
This is a very commonly tested pattern at senior level.
What Is IServiceScopeFactory and Why Is It Important for Background Services?
IServiceScopeFactory creates IServiceScope instances, which define a lifetime boundary. A scope created via IServiceScopeFactory.CreateScope() gives you an IServiceProvider that treats Scoped services as Scoped within that artificially created scope.
For background services (BackgroundService / IHostedService), this is the standard workaround for the "Singleton cannot directly depend on Scoped" rule. The hosted service (Singleton) injects IServiceScopeFactory, then creates a new scope per unit of work (e.g., per message processed from a queue), resolves the DbContext within that scope, does its work, and disposes the scope โ cleaning up the DbContext with it.
Microsoft's own documentation on background tasks with hosted services covers this pattern explicitly, and it's a near-certain topic in any senior interview that touches background processing.
How Do You Register Open Generic Types?
ASP.NET Core DI supports open generic registration, which is useful for cross-cutting patterns like repositories, validators, and event handlers:
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
When the container resolves IRepository<Order>, it automatically closes the generic and creates Repository<Order>. This eliminates repetitive per-entity registrations and is the backbone of most repository pattern implementations.
Interviewers at larger organisations specifically ask about this because it's a sign that you've worked on codebases with non-trivial domain models rather than toy apps.
Advanced: Senior-Level DI Scenarios
What Are Keyed Services in .NET 8+ and When Do You Use Them?
Keyed services, introduced in .NET 8, allow multiple implementations of the same interface to be registered under distinct string or enum keys. Before .NET 8, achieving this required named registrations via Autofac or a custom factory/dictionary pattern.
Registration:
services.AddKeyedScoped<INotificationChannel, EmailChannel>("email");
services.AddKeyedScoped<INotificationChannel, SmsChannel>("sms");
Resolution in a constructor uses [FromKeyedServices("email")]:
public NotificationRouter([FromKeyedServices("email")] INotificationChannel emailChannel) { ... }
This is a direct answer to "how do you implement a strategy pattern in ASP.NET Core DI without a third-party container" โ and it's one of the more modern .NET 8 DI features that interviewers are starting to test.
For broader context on how Keyed Services fit alongside other DI patterns, see our post on ASP.NET Core DI Lifetimes: Singleton vs. Scoped vs. Transient โ Enterprise Decision Guide.
What Is the Captive Dependency Problem and How Do You Detect It?
A captive dependency occurs when a long-lived service holds a reference to a shorter-lived one, effectively extending the shorter-lived service's lifetime beyond what was intended.
The classic example: a Singleton service injected with a Scoped DbContext. The DbContext was designed to live for one request. But because it's captured in a Singleton, it now lives for the application lifetime โ accumulating tracked entities, holding open connections, and producing incorrect change-tracking results.
Detection mechanisms:
- Development-mode scope validation: set
ValidateScopes = trueinUseDefaultServiceProvideroptions (enabled automatically inDevelopmentenvironment in ASP.NET Core) - Scope validation on build: call
services.BuildServiceProvider(new ServiceProviderOptions { ValidateScopes = true, ValidateOnBuild = true })in test bootstrapping to catch registration errors at startup
The ValidateOnBuild flag is particularly powerful: it eagerly validates all registrations at container build time, not lazily at first resolution. This catches misconfigured services before a single request is processed.
See RFC 7807 / Problem Details to understand how standardised error contracts from these failures should be surfaced in APIs.
How Would You Replace the Built-In DI Container with Autofac?
The built-in Microsoft.Extensions.DependencyInjection container covers the 80% case. When you need property injection, interceptors, delegate factories, or more complex lifetime rules, you can swap in Autofac.
The integration point is the IServiceProviderFactory<TContainerBuilder> abstraction. Autofac provides AutofacServiceProviderFactory, which you register in Program.cs:
builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
builder.Host.ConfigureContainer<ContainerBuilder>(containerBuilder => {
containerBuilder.RegisterModule<MyApplicationModule>();
});
Standard IServiceCollection registrations (from AddDbContext, AddControllers, etc.) are automatically forwarded to Autofac's container builder, so framework registrations are preserved. Your custom module adds to them.
This is a design decision question: if you ever hear "we need property injection everywhere" in an interview, the correct answer is to challenge the need first (constructor injection is almost always better), then explain the Autofac path if there's a genuine requirement.
How Does IOptions<T>, IOptionsSnapshot<T>, and IOptionsMonitor<T> Relate to DI?
The Options pattern is the idiomatic way to inject configuration into services in ASP.NET Core. All three interfaces are registered by AddOptions() (called automatically by the host):
| Interface | Lifetime | Reloads? | Use When |
|---|---|---|---|
IOptions<T> |
Singleton | โ | Static config that never changes |
IOptionsSnapshot<T> |
Scoped | โ (per request) | Per-request config, e.g. feature flags |
IOptionsMonitor<T> |
Singleton | โ (real-time) | Background services, long-running processes |
A senior-level gotcha: injecting IOptionsSnapshot<T> into a Singleton is the captive dependency problem in disguise. IOptionsSnapshot is Scoped, so it can't be safely used from Singleton services โ use IOptionsMonitor<T> instead.
Expert: DI Architecture and Design Questions
How Do You Design a Modular DI Registration Strategy for Large Applications?
Large applications should avoid a single monolithic Program.cs registration block. Two patterns address this:
1. Extension method per layer: Each layer (Data, Application, Infrastructure) exposes a services.AddDataLayer(configuration) extension method. Callers chain them at startup. The coupling stays at the composition root; the layers themselves don't know about each other's internals.
2. IModule / IServiceInstaller pattern: Define a marker interface and reflect over assemblies at startup to discover and invoke all module registrations automatically. This works well in plugin-based architectures and microservice shared libraries.
Both approaches keep registration logic co-located with the services it registers, make code reviews easier (change a service, change its registration in the same PR), and prevent the "who registered this?" archaeological digs that plague large codebases.
What Is the Composition Root and Why Does It Matter?
The Composition Root is the single location in an application where all dependencies are assembled โ in ASP.NET Core, this is Program.cs (or Startup.cs in older versions). The principle: only the Composition Root should ever call new on concrete types or resolve from IServiceProvider directly.
Keeping the composition root clean prevents the Service Locator anti-pattern from leaking into your domain and application layers. When dependencies are declared as constructor parameters throughout your codebase, the entire dependency graph becomes readable from the Composition Root alone โ a significant advantage when debugging DI resolution failures.
Mark Seemann's book Dependency Injection Principles, Practices, and Patterns is the canonical reference, and senior interviewers who care about architecture often reference it.
How Does DI Interact with Minimal APIs in .NET 10?
In Minimal APIs, dependencies can be injected directly into route handler lambdas via parameter binding โ the framework detects registered service types and injects them without explicit [FromServices] annotation (automatic service parameter inference).
However, for complex APIs with many handlers, this can create very large lambdas with many constructor-like parameters. The cleaner pattern is to group related handlers into endpoint classes that receive dependencies via constructor injection and register endpoints via extension methods on IEndpointRouteBuilder. This recovers the testability and organisation of controller-based APIs while keeping the Minimal API programming model.
A related consideration: endpoint filters in Minimal APIs can use constructor-injected DI services, making them a lightweight alternative to action filters for cross-cutting concerns. See our article on ASP.NET Core Middleware vs Action Filters vs Endpoint Filters for a full breakdown of where each belongs.
โ Prefer a one-time tip? Buy us a coffee โ every bit helps keep the content coming!
Frequently Asked Questions
What Is the Difference Between GetService<T> and GetRequiredService<T>?
GetService<T> returns null if the service is not registered; GetRequiredService<T> throws an InvalidOperationException. In production application code, prefer GetRequiredService<T> โ a missing registration is a programming error, not a recoverable runtime condition, and failing loudly at startup (or first resolution) is far better than a null reference exception buried deep in a request pipeline.
Can You Inject a Singleton into a Scoped Service?
Yes, and it works correctly. Injecting a longer-lived service (Singleton) into a shorter-lived one (Scoped or Transient) is safe. The Singleton outlives the Scoped service, so there's no lifetime mismatch. The problematic direction is the reverse: a Singleton holding a reference to a Scoped or Transient service (the captive dependency problem).
How Do You Unit Test a Class That Uses ASP.NET Core DI?
You don't test DI in unit tests โ you test the class itself in isolation. Replace dependencies with mocks (Moq, NSubstitute) and pass them via the constructor directly. DI is a concern of integration and end-to-end tests. In integration tests, you can use WebApplicationFactory<TProgram> to boot the actual DI container, override registrations with test doubles using ConfigureTestServices, and test the full request pipeline against a real container.
What Happens to IDisposable Services When They Are Resolved?
The DI container tracks and disposes IDisposable services when their owning scope is disposed. For Singleton services, disposal happens at application shutdown. For Scoped services, disposal occurs at the end of the HTTP request (when the request scope is disposed). For Transient services, the container does track and dispose them โ but only at scope disposal, not at resolution. If a Transient IDisposable is resolved multiple times within a scope, all instances are held until the scope closes. This means Transient should not be used for expensive disposable resources under high-concurrency scenarios.
What Is ValidateOnBuild and Should You Always Enable It?
ValidateOnBuild = true instructs the DI container to validate all registrations eagerly when BuildServiceProvider() is called, rather than lazily at first resolution. This means misconfigured services (missing registrations, lifetime violations) are caught at application startup โ before any request is served. The trade-off is a slightly slower startup time. The consensus for production applications: enable it in CI/CD pipeline test bootstrapping and staging environments. For pure production builds where startup speed matters, evaluate based on your specific startup latency requirements. For most enterprise web APIs, the startup overhead is negligible.
Can You Have Multiple Implementations of the Same Interface?
Yes. You can register multiple implementations:
services.AddScoped<INotificationHandler, EmailHandler>();
services.AddScoped<INotificationHandler, SmsHandler>();
When you inject IEnumerable<INotificationHandler>, the container resolves all registered implementations in registration order. This is the idiomatic way to implement the Composite or Chain of Responsibility patterns in ASP.NET Core. When you inject the interface directly (not IEnumerable), the container resolves the last registered implementation โ a behaviour that catches many developers off-guard.
What Is the Difference Between TryAdd and Add Registration Methods?
services.TryAddScoped<T>() only registers the service if no existing registration for T exists. services.AddScoped<T>() always adds a new registration, even if one already exists. Framework components like AddDbContext, AddIdentity, and AddHttpClient use TryAdd internally to be idempotent โ calling them twice doesn't create duplicate registrations. Use TryAdd when writing library code or shared modules that should not override consumer registrations.






