Microsoft.Extensions.DependencyInjection
Overview
Microsoft.Extensions.DependencyInjection is the built-in dependency injection (DI) container for .NET applications. It provides constructor injection, service lifetime management (singleton, scoped, transient), and a registration API through IServiceCollection. The container is deeply integrated with the .NET hosting model, ASP.NET Core, Entity Framework Core, and virtually all Microsoft.Extensions libraries.
The built-in container is intentionally simple and fast. It supports constructor injection but not property injection, interception, or convention-based registration. For those advanced features, third-party containers like Autofac or Microsoft.Extensions.DependencyInjection.Abstractions-compatible containers can be plugged in as replacements.
Basic Service Registration
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; var builder = Host.CreateApplicationBuilder(args); // Register by interface and implementation builder.Services.AddSingleton<IClock, SystemClock>(); builder.Services.AddScoped<IOrderRepository, SqlOrderRepository>(); builder.Services.AddTransient<IEmailSender, SmtpEmailSender>(); // Register a concrete type directly builder.Services.AddScoped<OrderService>(); var app = builder.Build(); await app.RunAsync();
Service Lifetimes
Understanding when to use each lifetime is critical for correctness.
using Microsoft.Extensions.DependencyInjection;
public static class ServiceRegistration
{
public static IServiceCollection AddApplicationServices(
this IServiceCollection services)
{
// Singleton: one instance for the entire application lifetime.
// Use for stateless services, configuration, caches, and connection pools.
services.AddSingleton<IConnectionPool, RedisConnectionPool>();
// Scoped: one instance per scope (per HTTP request in ASP.NET Core).
// Use for DbContext, Unit of Work, and request-scoped state.
services.AddScoped<IUnitOfWork, EfUnitOfWork>();
services.AddScoped<IUserContext, HttpUserContext>();
// Transient: a new instance every time it is requested.
// Use for lightweight, stateless services with no shared state.
services.AddTransient<IValidator<Order>, OrderValidator>();
services.AddTransient<INotificationBuilder, NotificationBuilder>();
return services;
}
}
| Lifetime | Instance Created | Disposed | Use When |
|---|---|---|---|
| Singleton | Once per application | At app shutdown | Stateless, thread-safe, or expensive to create |
| Scoped | Once per scope/request | At scope end | Request-specific state, DbContext |
| Transient | Every resolution | When scope ends (if IDisposable) | Lightweight, no shared state |
Factory-Based Registration
Use a factory delegate when construction requires runtime logic.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
public static class ServiceRegistration
{
public static IServiceCollection AddStorageServices(
this IServiceCollection services, IConfiguration configuration)
{
services.AddSingleton<IBlobStorage>(sp =>
{
string provider = configuration["Storage:Provider"] ?? "local";
return provider switch
{
"azure" => new AzureBlobStorage(
configuration["Storage:Azure:ConnectionString"]!),
"aws" => new S3BlobStorage(
configuration["Storage:Aws:Region"]!,
configuration["Storage:Aws:BucketName"]!),
_ => new LocalBlobStorage(
configuration["Storage:Local:Path"] ?? "/data")
};
});
return services;
}
}
Keyed Services (.NET 8+)
Keyed services allow registering multiple implementations of the same interface, differentiated by a key.
using Microsoft.Extensions.DependencyInjection;
public static class ServiceRegistration
{
public static IServiceCollection AddNotificationServices(
this IServiceCollection services)
{
services.AddKeyedSingleton<INotificationSender, EmailSender>("email");
services.AddKeyedSingleton<INotificationSender, SmsSender>("sms");
services.AddKeyedSingleton<INotificationSender, PushSender>("push");
return services;
}
}
using Microsoft.Extensions.DependencyInjection;
public sealed class NotificationService
{
private readonly INotificationSender _emailSender;
private readonly INotificationSender _smsSender;
public NotificationService(
[FromKeyedServices("email")] INotificationSender emailSender,
[FromKeyedServices("sms")] INotificationSender smsSender)
{
_emailSender = emailSender;
_smsSender = smsSender;
}
public async Task NotifyAsync(string userId, string message, CancellationToken ct)
{
await _emailSender.SendAsync(userId, message, ct);
await _smsSender.SendAsync(userId, message, ct);
}
}
Multiple Implementations of the Same Interface
When multiple implementations are registered, inject IEnumerable<T> to receive all of them.
using Microsoft.Extensions.DependencyInjection; // Registration builder.Services.AddSingleton<IHealthCheck, DatabaseHealthCheck>(); builder.Services.AddSingleton<IHealthCheck, RedisHealthCheck>(); builder.Services.AddSingleton<IHealthCheck, ExternalApiHealthCheck>();
// Consumption
public sealed class HealthCheckService
{
private readonly IEnumerable<IHealthCheck> _checks;
public HealthCheckService(IEnumerable<IHealthCheck> checks)
{
_checks = checks;
}
public async Task<HealthReport> CheckAllAsync(CancellationToken ct)
{
var results = new List<HealthCheckResult>();
foreach (IHealthCheck check in _checks)
{
HealthCheckResult result = await check.CheckAsync(ct);
results.Add(result);
}
return new HealthReport(results);
}
}
Decorator Pattern
Wrap an existing registration with additional behavior.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
public static class ServiceRegistration
{
public static IServiceCollection AddDecoratedRepository(
this IServiceCollection services)
{
// Register the base implementation
services.AddScoped<SqlOrderRepository>();
// Register the decorator, resolving the inner service from the container
services.AddScoped<IOrderRepository>(sp =>
{
var inner = sp.GetRequiredService<SqlOrderRepository>();
var logger = sp.GetRequiredService<ILogger<LoggingOrderRepository>>();
return new LoggingOrderRepository(inner, logger);
});
return services;
}
}
public sealed class LoggingOrderRepository : IOrderRepository
{
private readonly IOrderRepository _inner;
private readonly ILogger<LoggingOrderRepository> _logger;
public LoggingOrderRepository(IOrderRepository inner, ILogger<LoggingOrderRepository> logger)
{
_inner = inner;
_logger = logger;
}
public async Task<Order?> GetByIdAsync(int id, CancellationToken ct)
{
_logger.LogDebug("Getting order {OrderId}", id);
var order = await _inner.GetByIdAsync(id, ct);
_logger.LogDebug("Order {OrderId} found: {Found}", id, order is not null);
return order;
}
// ... delegate remaining methods to _inner
}
Organizing Registrations with Extension Methods
using Microsoft.Extensions.DependencyInjection;
public static class DataAccessExtensions
{
public static IServiceCollection AddDataAccess(
this IServiceCollection services, string connectionString)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(connectionString));
services.AddScoped<IOrderRepository, SqlOrderRepository>();
services.AddScoped<IProductRepository, SqlProductRepository>();
services.AddScoped<ICustomerRepository, SqlCustomerRepository>();
return services;
}
}
public static class MessagingExtensions
{
public static IServiceCollection AddMessaging(this IServiceCollection services)
{
services.AddSingleton<IMessageBus, RabbitMqMessageBus>();
services.AddTransient<IEmailSender, SmtpEmailSender>();
return services;
}
}
// Program.cs stays clean
var builder = Host.CreateApplicationBuilder(args);
builder.Services
.AddDataAccess(builder.Configuration.GetConnectionString("Default")!)
.AddMessaging();
var app = builder.Build();
await app.RunAsync();
Validating the Container
Detect missing registrations at startup rather than at runtime.
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddApplicationServices();
// In Development, validate that all services can be resolved
if (builder.Environment.IsDevelopment())
{
builder.Host.UseDefaultServiceProvider(options =>
{
options.ValidateScopes = true; // Detect scoped-in-singleton bugs
options.ValidateOnBuild = true; // Verify all registrations resolve
});
}
var app = builder.Build();
await app.RunAsync();
Best Practices
- •Organize service registrations into focused
IServiceCollectionextension methods grouped by feature or layer (e.g.,AddDataAccess,AddMessaging) and call them fromProgram.cs. - •Choose lifetimes based on state: use singleton for stateless or thread-safe services, scoped for request-specific state like
DbContext, and transient only for lightweight, no-state objects. - •Never inject a scoped service into a singleton service -- this causes the scoped service to act as a singleton (captive dependency); enable
ValidateScopesin development to detect this. - •Enable
ValidateOnBuildin development environments to catch missing registrations at startup rather than discovering them at runtime throughInvalidOperationException. - •Prefer constructor injection over
IServiceProvider.GetService(service locator pattern); injectingIServiceProviderhides dependencies and makes code harder to test. - •Use keyed services (
.AddKeyedSingleton,[FromKeyedServices]) in .NET 8+ instead of custom factory patterns when the same interface has multiple implementations selected by name. - •Register
IDisposableandIAsyncDisposableservices through the container rather than creating them manually, so the container manages their disposal at the correct time. - •Inject
IEnumerable<T>to receive all registered implementations of an interface, useful for plugin systems, validation chains, and health checks. - •Avoid registering services with both a concrete type and an interface separately if they should share the same instance; use a forwarding registration pattern instead.
- •Write integration tests that build the real
IServiceProviderfrom your registration code and callGetRequiredService<T>for critical services to verify the container is wired correctly.