# 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.

These questions get easier once you have seen the patterns in a real codebase. The complete dependency-injection setup - lifetimes, keyed services, and clean composition across layers - is on [Patreon](https://www.patreon.com/CodingDroplets), ready to run. [Chapter 11 of the ASP.NET Core Web API: Zero to Production course](https://aspnetcoreapi.codingdroplets.com/) shows DI wired through a four-layer Clean Architecture with MediatR, which is exactly the context senior interviews probe.

[![ASP.NET Core Web API: Zero to Production](https://newsletter.codingdroplets.com/images/aspnet-core-api-course-banner-1.jpg)](https://aspnetcoreapi.codingdroplets.com/)

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 `DbContext` and 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:

- `AddSingleton` also has an overload that accepts a pre-constructed instance (`services.AddSingleton<ICache>(myExistingCache)`), which is useful for objects that pre-exist the container startup
- `AddScoped` is the right default for database contexts and anything involving per-request state
- `AddTransient` is often overused; watch for transient services that hold unmanaged resources - the container disposes `IDisposable` transient 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 `IHostedService` implementations 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](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/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](https://codingdroplets.com/aspnet-core-di-lifetimes-singleton-scoped-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 = true` in `UseDefaultServiceProvider` options (enabled automatically in `Development` environment 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](https://www.rfc-editor.org/rfc/rfc7807) 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](https://codingdroplets.com/aspnet-core-middleware-vs-action-filters-vs-endpoint-filters-enterprise-guide) for a full breakdown of where each belongs.

---

## 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.

* * *

## About the Author

**Celin Daniel** is Co-founder of Coding Droplets with 13+ years of hands-on experience building, shipping, and operating .NET and ASP.NET Core systems in production. The guidance here comes from real projects and production incidents, not theory.

- Website: [codingdroplets.com](https://codingdroplets.com/)
- GitHub: [github.com/codingdroplets](http://github.com/codingdroplets/)
- YouTube: [youtube.com/@CodingDroplets](https://www.youtube.com/@CodingDroplets)
