In a healthcare SaaS platform handling 5M+ daily API calls, you run an ASP.NET Core service that processes patient scheduling, billing, and audit logging. The system must meet strict HIPAA compliance, and production incidents often trace back to subtle dependency injection (DI) misconfigurations: incorrect lifetimes causing memory leaks, accidental singleton state, and services resolving the wrong implementation in certain environments.
Explain how you handle dependency injection in an ASP.NET Core application.
In your answer, cover:
AddSingleton, AddScoped, AddTransient) and how ASP.NET Core resolves dependencies for controllers, middleware, hosted services, and background jobs.Assume the interviewer expects Staff-level depth: discuss container behavior under the hood, common pitfalls in high-throughput services, and practical patterns (options pattern, typed HTTP clients, factories) that keep dependencies explicit and safe.
In ASP.NET Core, DI configuration typically lives in a single place (Program.cs / Startup.cs), often called the composition root. Keeping registrations centralized prevents hidden coupling and makes environment-specific wiring (dev vs prod) explicit.
builder.Services.AddScoped<IClock, SystemClock>();
builder.Services.AddSingleton<IAuditSink, AuditSink>();
Singleton is one instance for the app lifetime, scoped is one per request (or per explicit scope), and transient is a new instance per resolution. Choosing the wrong lifetime can cause shared mutable state, thread-safety bugs, or excessive allocations.
services.AddSingleton<ICache, MemoryCache>();
services.AddScoped<IUnitOfWork, UnitOfWork>();
services.AddTransient<IValidator, Validator>();
ASP.NET Core creates a scope per HTTP request; scoped services are tied to that scope and disposed at the end. For background work, you must create your own scope via IServiceScopeFactory to safely resolve scoped dependencies.
using var scope = scopeFactory.CreateScope();
var uow = scope.ServiceProvider.GetRequiredService<IUnitOfWork>();
Injecting IServiceProvider everywhere hides dependencies and pushes errors to runtime. Prefer constructor injection so dependencies are explicit, and use factories (Func, abstract factory, or typed clients) when runtime selection is required.
public BillingService(IPaymentGateway gateway, IClock clock) { ... }
Configuration should be injected as strongly typed options (IOptions/IOptionsMonitor) rather than reading configuration ad hoc. For outbound calls, IHttpClientFactory with typed clients avoids socket exhaustion and centralizes resilience policies.
services.Configure<PaymentsOptions>(config.GetSection("Payments"));
services.AddHttpClient<IPaymentsClient, PaymentsClient>();