Dependency injection questions separate developers who can call AddScoped from developers who understand what the container is actually guaranteeing them. Every senior and lead .NET interview eventually asks about lifetimes, because lifetime mistakes are the single most common cause of hard-to-reproduce production bugs: a DbContext shared across concurrent requests, a background service that silently stops seeing updated data, or a memory leak from services resolved off the root container. Interviewers use DI questions to probe whether a candidate reasons about object graphs and ownership, not just registration syntax. The ten questions below cover lifetimes and captive dependencies, scope validation, consuming scoped services from singletons and background services, keyed services, decorators, disposal rules, factories, the service locator and over-injection anti-patterns, and when a third-party container earns its keep.

Q1 Explain the three DI lifetimes and the rule that a service should only depend on services with an equal or longer lifetime. What happens when you break it?#

Short answer: Transient services are created on every resolution, scoped services once per scope (one HTTP request in ASP.NET Core by default), and singletons once for the life of the container; a service should never depend on one with a shorter lifetime, because the container has no way to hand a singleton a "fresh" scoped instance later, so it just keeps the first one forever.

The lifetime you pick is a correctness decision about who shares state, not a performance tuning knob. A transient is cheap to reason about but expensive to create in volume; a singleton is cheap to create but must be thread-safe for the life of the process; a scoped instance sits in between, sharing state within one unit of work and nothing else.

C#
// A singleton that captures a scoped DbContext is a ticking bug, not a design choice.
public sealed class PriceCache(ShopDbContext db) // db is scoped
{
    // The FIRST request's DbContext is now shared by every subsequent caller,
    // forever, on a type that isn't thread-safe.
}

builder.Services.AddDbContext<ShopDbContext>(...);       // Scoped by default
builder.Services.AddSingleton<PriceCache>();               // Captures the scoped context

Breaking the rule is called a captive dependency, covered in depth in the next question. What breaks in practice is concurrency: two requests hitting the singleton concurrently now share one DbContext, which throws InvalidOperationException about a second operation starting before the first completed, or silently corrupts tracked entities.

What interviewers look for: the lifetime-as-ownership framing rather than "singleton is fastest," and the ability to state the dependency direction rule without prompting.

Common mistakes: treating lifetime choice as a performance optimization instead of a sharing decision; assuming the container will "figure it out" when lifetimes are mismatched.

Follow-up questions:

  • Is it safe for a transient service to depend on a singleton? Why?
  • What does the container guarantee about thread safety, and what doesn't it guarantee?

Q2 What is a captive dependency, and how do ValidateScopes and ValidateOnBuild catch it?#

Short answer: A captive dependency is a longer-lived service, almost always a singleton, holding a reference to a shorter-lived one, almost always scoped, so the short-lived instance is never refreshed or disposed as intended; ValidateScopes makes resolving a scoped service from the root provider or from inside a singleton throw immediately, and ValidateOnBuild walks every registration's graph at startup so a bad dependency fails the deployment instead of the first request that needs it.

C#
var builder = WebApplication.CreateBuilder(args);

builder.Host.UseDefaultServiceProvider(options =>
{
    options.ValidateScopes = true; // On by default only in Development
    options.ValidateOnBuild = true;
});

Both flags are enabled automatically in the Development environment by the default host builders, which is exactly why this bug class so often survives code review and local testing: the safety net is off in Staging and Production unless a team turns it on deliberately, and it should be turned on in integration tests too, where it catches the same mistakes before a release. The most common real-world instance is a singleton that injects IDbContextFactory<TContext> incorrectly, still capturing a DbContext instance rather than the factory, or a caching singleton that takes IOptionsSnapshot<T>, which is itself scoped, instead of IOptionsMonitor<T>.

What interviewers look for: naming both flags and explaining that they run at different times (build time vs. resolution time), plus the observation that Development-only defaults mean this bug ships silently unless CI enables the same validation.

Common mistakes: assuming ValidateOnBuild catches everything; it cannot see inside factory delegates, so a graph built by a factory is only checked the first time something resolves it.

Follow-up questions:

  • Why doesn't ValidateOnBuild catch mistakes hidden behind a factory delegate?
  • What's the fix once you've found a captive dependency, without just making the singleton scoped?

Q3 How do you safely use a scoped service, such as a DbContext, from inside a singleton or a BackgroundService?#

Short answer: Inject IServiceScopeFactory, create a new scope for each unit of work with CreateAsyncScope(), resolve the scoped service from that scope's provider, and dispose the scope when the work finishes; for EF Core specifically, IDbContextFactory<TContext> is a simpler, purpose-built alternative that skips manual scope management.

C#
public sealed class NightlyExportService(IServiceScopeFactory scopeFactory) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await using (var scope = scopeFactory.CreateAsyncScope())
            {
                var exporter = scope.ServiceProvider.GetRequiredService<ReportExporter>();
                await exporter.RunAsync(stoppingToken);
            } // Scoped services, including any DbContext, are disposed here

            await Task.Delay(TimeSpan.FromHours(24), stoppingToken);
        }
    }
}

// Or, for EF Core specifically, skip scope management entirely:
builder.Services.AddDbContextFactory<ShopDbContext>(o => o.UseSqlServer(connectionString));

public sealed class PriceCache(IDbContextFactory<ShopDbContext> factory)
{
    public async Task<decimal?> GetPriceAsync(string sku, CancellationToken ct)
    {
        await using var db = await factory.CreateDbContextAsync(ct);
        return await db.Products.Where(p => p.Sku == sku).Select(p => (decimal?)p.Price)
            .FirstOrDefaultAsync(ct);
    }
}

IHostedService and BackgroundService are themselves registered as singletons, which is exactly why they can never take a scoped dependency directly through their constructor; they must create their own scope per iteration of work, the same pattern ASP.NET Core itself uses once per HTTP request. Dispose the scope asynchronously with await using whenever any resolved service implements IAsyncDisposable; disposing it synchronously throws.

What interviewers look for: the scope-per-unit-of-work pattern stated precisely, and awareness that hosted services are singletons by construction, which is why this problem exists in the first place.

Common mistakes: injecting a scoped service straight into a BackgroundService constructor, which either fails validation or silently becomes a captive dependency; disposing an async scope synchronously.

Follow-up questions:

  • Why are IHostedService implementations always singletons?
  • When would IDbContextFactory<TContext> be a better fit than IServiceScopeFactory?

Q4 What problem do keyed services solve, and how would you use them to select an implementation at runtime?#

Short answer: Keyed services, added in .NET 8, let you register several implementations of the same interface under distinct keys and resolve a specific one by key, replacing the hand-written factory delegates or dictionaries teams used to write for the same purpose.

C#
builder.Services.AddKeyedSingleton<IPaymentGateway, StripeGateway>("stripe");
builder.Services.AddKeyedSingleton<IPaymentGateway, AdyenGateway>("adyen");

public sealed class RefundService([FromKeyedServices("stripe")] IPaymentGateway gateway)
{
    public Task RefundAsync(string paymentId, CancellationToken ct) =>
        gateway.RefundAsync(paymentId, ct);
}

// Runtime selection, for example from a route value:
app.MapPost("/refunds/{provider}/{paymentId}",
    (string provider, string paymentId, IServiceProvider sp, CancellationToken ct) =>
        sp.GetRequiredKeyedService<IPaymentGateway>(provider).RefundAsync(paymentId, ct));

The key can be any object, though strings and enums are typical, and an implementation can receive its own key back through [ServiceKey], which is handy for logging which variant handled a call. .NET 10 added a parameterless form of [FromKeyedServices] that inherits the key of the service being constructed, useful for building a whole object graph per tenant without repeating the key at every layer, and tightened GetKeyedService with KeyedService.AnyKey to throw rather than silently returning one arbitrary match; enumerating all matches under AnyKey now requires GetKeyedServices (plural). Reach for keys when you have genuine variants of one abstraction, such as providers or tenants; if the "variants" are actually unrelated responsibilities, separate interfaces are the clearer design.

What interviewers look for: knowing when keyed services arrived and what they replaced, plus a runtime-selection example, not just a static [FromKeyedServices("x")] on one constructor.

Common mistakes: using keys to paper over what should be two distinct interfaces; forgetting that GetKeyedService (singular) with AnyKey throws as of .NET 10, rather than returning the first match.

Follow-up questions:

  • How would you provide a fallback implementation for any key that has no explicit registration?
  • What changed about [FromKeyedServices] in .NET 10?

Q5 How do you implement the decorator pattern with the built-in container, and what does Scrutor add?#

Short answer: The built-in container has no Decorate method, so a manual decorator typically registers the inner implementation under a key and the decorator under the plain interface, taking the keyed inner instance as a constructor parameter; the open-source Scrutor library adds a genuine Decorate extension plus assembly scanning on top of IServiceCollection without replacing the container.

C#
// Manual decorator using a keyed inner service.
builder.Services.AddKeyedScoped<IProductCatalog, SqlProductCatalog>("inner");
builder.Services.AddScoped<IProductCatalog, CachedProductCatalog>();

public sealed class CachedProductCatalog(
    [FromKeyedServices("inner")] IProductCatalog inner, IMemoryCache cache) : IProductCatalog
{
    public Task<Product?> FindAsync(string sku, CancellationToken ct) =>
        cache.GetOrCreateAsync($"product:{sku}", entry =>
        {
            entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5);
            return inner.FindAsync(sku, ct);
        });
}

// With Scrutor: the decorator takes a plain IProductCatalog and Decorate() wires the chain.
builder.Services.AddScoped<IProductCatalog, SqlProductCatalog>();
builder.Services.Decorate<IProductCatalog, CachedProductCatalog>();

Decorators are the cleanest way to add caching, retries or authorization around an existing implementation without touching its code, which matters in interviews because it tests whether a candidate can compose behavior instead of reaching for inheritance or if/else branching inside the original class. Scrutor's scanning is a separate but related feature; it registers whole families of similar types, such as validators or handlers, by convention instead of one Add* call per type.

What interviewers look for: a correct manual implementation using keyed services (the modern idiom) rather than an outdated factory-delegate workaround, and clarity on what Scrutor adds versus what the built-in container already does.

Common mistakes: trying to register the same interface twice expecting the container to "chain" them; it resolves only the last registration for single-instance lookups, so a manual decorator needs the keyed-inner pattern or Scrutor.

Follow-up questions:

  • How would you decorate a service with two layers, such as caching and retry, in the correct order?
  • What does TryAddEnumerable protect against that plain AddScoped doesn't?

Q6 What are the disposal rules for services the container creates versus instances you register yourself, and what happens with IAsyncDisposable?#

Short answer: The container disposes anything it creates, transient and scoped instances when their scope ends and singletons when the root provider is disposed at shutdown, but it never disposes an instance you hand it directly through AddSingleton(instance), because it doesn't own that object's lifetime; and if a scope contains a service that implements only IAsyncDisposable, disposing that scope synchronously throws an InvalidOperationException.

C#
// Not disposed by the container: you own this instance's lifetime.
var httpClient = new HttpClient();
builder.Services.AddSingleton(httpClient);

public sealed class ReportJob(IServiceScopeFactory scopeFactory)
{
    public async Task RunAsync(CancellationToken ct)
    {
        await using var scope = scopeFactory.CreateAsyncScope(); // Must be async-disposed
        var exporter = scope.ServiceProvider.GetRequiredService<AsyncOnlyExporter>();
        await exporter.ExportAsync(ct);
    } // DisposeAsync runs here; a plain `using` would throw for an IAsyncDisposable-only service
}

Two incident patterns come from getting this wrong. First, a transient IDisposable resolved from the root provider, rather than a per-request scope, is tracked by the root and never released until the application shuts down, which is a slow, hard-to-spot memory leak rather than a crash. Second, never call Dispose() on a service you received through constructor injection; the container owns shared instances, and disposing one out from under other consumers breaks them in ways that are difficult to trace back to the disposal call.

What interviewers look for: the ownership rule stated precisely (the container disposes only what it built), and the specific InvalidOperationException behavior for mixed sync/async disposal, which shows hands-on experience rather than documentation recall.

Common mistakes: resolving disposable transients from the root container in a loop, silently leaking memory; calling Dispose() manually on an injected dependency.

Follow-up questions:

  • Why does resolving a disposable transient from the root provider leak memory specifically?
  • How would you detect this leak in production before it becomes an incident?

Q7 When is injecting IServiceProvider a legitimate pattern, and when does it become the service locator anti-pattern? What about a constructor with fifteen dependencies?#

Short answer: Injecting IServiceProvider is legitimate in composition code, factories and scope creation, anywhere the exact type to resolve is only known at runtime, but it becomes the service locator anti-pattern the moment ordinary business logic calls GetRequiredService to fetch its own collaborators instead of declaring them as constructor parameters, because that hides the class's real dependencies and defers missing-registration failures from startup to whenever that code path finally runs.

A constructor with fifteen parameters is a related but distinct smell: it's not automatically wrong, dependency injection is doing its job by making every collaborator explicit, but it's strong evidence the class has too many responsibilities. In review, the fix is rarely "inject IServiceProvider to shorten the constructor"; that just hides the same fifteen dependencies behind a locator call and makes the class harder to unit test. The better fix is usually to split the class along its responsibilities, or to introduce a smaller façade service that bundles a cohesive subset of those dependencies for classes that need several of them together.

C#
// Service locator: hides real dependencies, fails at runtime, hard to unit test.
public sealed class OrderService(IServiceProvider provider)
{
    public async Task PlaceAsync(Order order)
    {
        var repo = provider.GetRequiredService<IOrderRepository>(); // Hidden dependency
        await repo.SaveAsync(order);
    }
}

// Explicit dependencies: testable, fails fast at startup if misconfigured.
public sealed class OrderService(IOrderRepository repository)
{
    public Task PlaceAsync(Order order) => repository.SaveAsync(order);
}

What interviewers look for: a precise boundary between "legitimate provider use" and "service locator," not a blanket "never inject IServiceProvider," plus a constructive answer to the long-constructor question that isn't just "use IServiceProvider instead."

Common mistakes: treating any use of IServiceProvider as automatically wrong; treating a long constructor as purely a DI problem instead of a design smell to address by splitting responsibilities.

Follow-up questions:

  • Where in a real ASP.NET Core app does the framework itself legitimately use IServiceProvider like a locator?
  • How would you refactor a fifteen-dependency constructor without hiding the dependencies?

Q8 How do factories fit into DI, and what mistakes do teams make with them?#

Short answer: Factory delegates are the right tool when construction depends on runtime data, such as configuration choosing between implementations, or when a third-party type has a constructor the container can't satisfy on its own; ActivatorUtilities.CreateInstance builds an unregistered type by mixing explicit arguments with services pulled from the provider, but factories must stay synchronous and side-effect free, because blocking on an async operation inside one is a common source of deadlocks.

C#
builder.Services.AddSingleton<IBlobStore>(sp =>
{
    var options = sp.GetRequiredService<IOptions<StorageOptions>>().Value;
    return options.UseLocalDisk
        ? new DiskBlobStore(options.RootPath)
        : ActivatorUtilities.CreateInstance<AzureBlobStore>(sp, options.ContainerName);
});

Most registrations should still use implementation types rather than factories, because the container can analyze and validate an implementation type's constructor graph at build time; a factory delegate is opaque to that analysis, so ValidateOnBuild can't see what's inside it, and any missing dependency only surfaces the first time the factory actually runs. When a service genuinely needs asynchronous setup, the fix is not an async factory that calls .Result to force it synchronous; register the service normally and either expose an explicit InitializeAsync method the caller awaits, or perform the setup in a hosted service that runs once at startup.

What interviewers look for: the synchronous, side-effect-free constraint on factories stated as a deadlock-avoidance rule, not just a style preference, plus knowing that factories reduce what ValidateOnBuild can verify.

Common mistakes: blocking on .Result or .Wait() inside a factory delegate; using a factory everywhere out of habit instead of registering a plain implementation type.

Follow-up questions:

  • Why can ValidateOnBuild not fully validate a graph built through a factory delegate?
  • How would you handle a service that genuinely needs an async call before it's ready to use?

Q9 IEnumerable<T> resolves every registration, while T resolves the last one. What bugs does that create, and how do TryAdd* and TryAddEnumerable help?#

Short answer: Asking the container for T returns only the most recently registered implementation, which is what makes "register again to override" work, while asking for IEnumerable<T> returns every registration in order; the bug shows up when a library and an application both register the same interface expecting single-instance semantics, and one silently shadows the other, or when a pipeline built from IEnumerable<T> picks up unintended duplicates because two packages both registered the same handler.

C#
// TryAddSingleton only registers if nothing already claimed the interface.
builder.Services.TryAddSingleton<IClock, SystemClock>();       // Library default
builder.Services.AddSingleton<IClock, FakeClock>();             // App override always wins

// TryAddEnumerable checks the implementation type too, so calling this twice from
// two different feature modules doesn't duplicate the validator in IEnumerable<T>.
builder.Services.TryAddEnumerable(
    ServiceDescriptor.Transient<IValidator<Order>, OrderTotalValidator>());

TryAdd* registers a service only if nothing has already claimed that type, which lets library code provide sane defaults that applications can override simply by registering first or last, depending on which method they call; TryAddEnumerable additionally checks the implementation type, so calling the same registration method twice, for instance from two feature modules that both reference a shared library, doesn't produce duplicate entries in IEnumerable<T>. For deliberate overrides, Replace and RemoveAll are more explicit than relying on registration order, which is easy to get wrong as a codebase grows and registrations move between files.

What interviewers look for: the precise single-vs-enumerable resolution rule, and the TryAdd* family as the answer to "how do library authors avoid breaking consumers," which is a strong signal of real library or platform experience.

Common mistakes: assuming AddScoped calls are idempotent; not realizing that two calls to the same registration extension method can silently double an IEnumerable<T> pipeline.

Follow-up questions:

  • Why is TryAddEnumerable necessary in addition to TryAdd*?
  • How would you debug an IEnumerable<IValidator<T>> that's running a validator twice?

Q10 When, if ever, would you replace the built-in container with a third-party one like Autofac? What do you gain and give up?#

Short answer: Reach for a third-party container only when you need a concrete feature the built-in one lacks, such as property injection, child containers, or more advanced lifetime and scanning features, because swapping providers plugs in cleanly through IServiceProviderFactory<TContainerBuilder> but adds an adapter layer, a second registration dialect for the team to learn, and a dependency that must track every framework release on its own schedule.

C#
using Autofac;
using Autofac.Extensions.DependencyInjection;

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
builder.Host.ConfigureContainer<ContainerBuilder>(container =>
    container.RegisterModule(new BillingModule()));
NeedBuilt-in containerThird-party (for example Autofac)
Constructor injection, three lifetimesYesYes
Keyed servicesYes, since .NET 8Yes
DecoratorsManual, or via ScrutorUsually built in
Property injectionNoYes
Child containers, custom lifetimesNoYes
Ships and is tested with the frameworkYesMaintained separately

The strongest interview answer treats this as a cost-benefit call rather than a preference: the built-in container is what every ASP.NET Core, EF Core and worker-service feature is tested against, so staying on it avoids a whole class of integration surprises, and most teams that think they need a third-party container actually need Scrutor for decorators and scanning, which sits on top of the built-in one without replacing it.

What interviewers look for: a concrete, named feature gap driving the decision, not "it's more powerful"; awareness of the ongoing maintenance cost of a second DI dialect.

Common mistakes: switching containers early in a project "just in case," then never using the extra features it provides.

Follow-up questions:

  • What concrete feature would justify Autofac on a project you've worked on?
  • How does IServiceProviderFactory<TContainerBuilder> keep framework registrations working after a container swap?

Quick-Fire Round#

QuestionAnswer
Which lifetime is created once per HTTP request by default?Scoped
What's the classic captive dependency?A singleton holding a scoped DbContext
Which flags catch captive dependencies, and when do they run?ValidateScopes (resolution time), ValidateOnBuild (startup)
How does a BackgroundService safely use a scoped service?IServiceScopeFactory.CreateAsyncScope() per unit of work
When were keyed services added?.NET 8, with AddKeyedSingleton/Scoped/Transient
Does the built-in container support decorators natively?No; use keyed services manually or Scrutor's Decorate
Does the container dispose an instance passed to AddSingleton(instance)?No, it doesn't own instances you supply
What does T resolve to versus IEnumerable<T>?T: the last registration. IEnumerable<T>: all of them, in order
What's the risk in async factory delegates?Blocking with .Result/.Wait() can deadlock
When is IServiceProvider injection legitimate?Factories, scope creation, runtime keyed selection; not ordinary business logic

How to Prepare#

  • Be ready to draw the lifetime-dependency rule (never depend on something shorter-lived) and name a real captive dependency you've fixed.
  • Know the exact mechanics of IServiceScopeFactory versus IDbContextFactory<T> for singletons and background services.
  • Practice the keyed-services syntax from memory; it's a frequent live-coding ask since .NET 8 shipped it.
  • Be able to write a manual decorator with keyed services, not just describe Scrutor's Decorate method.
  • Have one clear example of a constructor that grew too large, and describe how you split it rather than papering over it with IServiceProvider.