Dependency Injection in ASP.NET Core: Mastering Service Lifetimes and Patterns
_cache[key] = value; // Thread-safe }
public T Get
## Captive Dependencies (Anti-Pattern)
### The Problem
**❌ Singleton capturing Scoped dependency:**
```csharp
// DON'T DO THIS
builder.Services.AddSingleton<CacheService>();
builder.Services.AddScoped<AppDbContext>();
public class CacheService // Singleton
{
```text
private readonly AppDbContext _context; // Scoped - PROBLEM!
public CacheService(AppDbContext context)
{
_context = context;
// DbContext stays alive for app lifetime
// Memory leak, stale data, threading issues
}```
}
The Solution
✅ Use IServiceScopeFactory:
// Correct approach
builder.Services.AddSingleton<CacheService>();
builder.Services.AddScoped<AppDbContext>();
public class CacheService
{
```csharp
private readonly IServiceScopeFactory _scopeFactory;
public CacheService(IServiceScopeFactory scopeFactory)
{
_scopeFactory = scopeFactory;
}
public async Task<User> GetUserAsync(int id)
{
using var scope = _scopeFactory.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();
return await context.Users.FindAsync(id);
}```
}
Keyed Services (.NET 8)
Figure: Program.cs – service registration with IntelliSense for DI lifetimes.
Registration
Multiple implementations:
// Register multiple implementations with keys
builder.Services.AddKeyedScoped<IPaymentProcessor, StripeProcessor>("stripe");
builder.Services.AddKeyedScoped<IPaymentProcessor, PayPalProcessor>("paypal");
builder.Services.AddKeyedScoped<IPaymentProcessor, SquareProcessor>("square");
Consumption
Inject by key:
// Option 1: Constructor injection with [FromKeyedServices]
public class PaymentController : ControllerBase
{
```text
private readonly IPaymentProcessor _stripeProcessor;
private readonly IPaymentProcessor _paypalProcessor;
public PaymentController(
[FromKeyedServices("stripe")] IPaymentProcessor stripeProcessor,
[FromKeyedServices("paypal")] IPaymentProcessor paypalProcessor)
{
_stripeProcessor = stripeProcessor;
_paypalProcessor = paypalProcessor;
}```
}
// Option 2: Resolve at runtime
public class PaymentService
{
```csharp
private readonly IServiceProvider _serviceProvider;
public PaymentService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public async Task ProcessPaymentAsync(Order order)
{
var processor = _serviceProvider
.GetRequiredKeyedService<IPaymentProcessor>(order.PaymentMethod);
await processor.ProcessAsync(order.Total);
}```
}
Factory Patterns
Simple Factory
Factory service:
public interface INotificationFactory
{
```text
INotificationService Create(NotificationType type);```
}
public class NotificationFactory : INotificationFactory
{
```javascript
private readonly IServiceProvider _serviceProvider;
public NotificationFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public INotificationService Create(NotificationType type)
{
return type switch
{
NotificationType.Email => _serviceProvider.GetRequiredService<EmailNotificationService>(),
NotificationType.Sms => _serviceProvider.GetRequiredService<SmsNotificationService>(),
NotificationType.Push => _serviceProvider.GetRequiredService<PushNotificationService>(),
_ => throw new ArgumentException($"Unknown notification type: {type}")
};
}```
}
// Registration
builder.Services.AddTransient<EmailNotificationService>();
builder.Services.AddTransient<SmsNotificationService>();
builder.Services.AddTransient<PushNotificationService>();
builder.Services.AddSingleton<INotificationFactory, NotificationFactory>();
Func Factory
Delegate injection:
// Registration
builder.Services.AddTransient<ExpensiveService>();
builder.Services.AddSingleton<Func<ExpensiveService>>(sp =>
```javascript
() => sp.GetRequiredService<ExpensiveService>());
// Usage public class WorkerService {
private readonly Func<ExpensiveService> _serviceFactory;
public WorkerService(Func<ExpensiveService> serviceFactory)
{
_serviceFactory = serviceFactory;
}
public async Task ProcessBatchAsync(List<Item> items)
{
foreach (var item in items)
{
// Create new instance for each item
var service = _serviceFactory();
await service.ProcessAsync(item);
}
}```
}
Named Options Pattern
Multiple configurations:
// Configuration
builder.Services.Configure<SmtpSettings>("Primary",
```text
builder.Configuration.GetSection("Smtp:Primary"));```
builder.Services.Configure<SmtpSettings>("Backup",
```text
builder.Configuration.GetSection("Smtp:Backup"));
// Usage public class EmailService {
private readonly IOptionsMonitor<SmtpSettings> _options;
public EmailService(IOptionsMonitor<SmtpSettings> options)
{
_options = options;
}
public async Task SendAsync(string to, string body, string server = "Primary")
{
var settings = _options.Get(server);
await SendEmailAsync(to, body, settings);
}```
}
Registration Patterns
Extension Methods
Organized registration:
// ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions
{
```csharp
public static IServiceCollection AddApplicationServices(
this IServiceCollection services)
{
services.AddScoped<IOrderService, OrderService>();
services.AddScoped<ICustomerService, CustomerService>();
services.AddScoped<IInventoryService, InventoryService>();
return services;
}
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("Default")));
services.AddScoped<IOrderRepository, OrderRepository>();
services.AddScoped<ICustomerRepository, CustomerRepository>();
return services;
}```
}
// Program.cs
builder.Services
```text
.AddApplicationServices()
.AddInfrastructure(builder.Configuration);
### TryAdd Methods
**Conditional registration:**
```csharp
// Register only if not already registered
services.TryAddScoped<IEmailService, EmailService>();
// Try add multiple
services.TryAddEnumerable(
```text
ServiceDescriptor.Scoped<IHostedService, MetricsService>());
// Replace registration services.Replace(ServiceDescriptor.Singleton<ICache, RedisCache>());
// Remove registration
services.RemoveAll
### Assembly Scanning
**Auto-registration:**
```csharp
// Register all implementations of an interface
public static IServiceCollection AddServicesFromAssembly(
```csharp
this IServiceCollection services,
Assembly assembly)```
{
```javascript
var serviceTypes = assembly.GetTypes()
.Where(t => t.IsClass && !t.IsAbstract)
.SelectMany(t => t.GetInterfaces(), (impl, iface) => new { impl, iface })
.Where(x => x.iface.Name == $"I{x.impl.Name}");
foreach (var service in serviceTypes)
{
services.AddScoped(service.iface, service.impl);
}
return services;```
}
// Usage
builder.Services.AddServicesFromAssembly(typeof(Program).Assembly);
Decorator Pattern
Wrapping services:
// Base service
public interface IOrderService
{
```text
Task<Order> CreateOrderAsync(Order order);```
}
public class OrderService : IOrderService
{
```csharp
public async Task<Order> CreateOrderAsync(Order order)
{
// Core logic
return order;
}```
}
// Decorator with logging
public class LoggingOrderService : IOrderService
{
```csharp
private readonly IOrderService _inner;
private readonly ILogger<LoggingOrderService> _logger;
public LoggingOrderService(
IOrderService inner,
ILogger<LoggingOrderService> logger)
{
_inner = inner;
_logger = logger;
}
public async Task<Order> CreateOrderAsync(Order order)
{
_logger.LogInformation("Creating order {OrderId}", order.Id);
var result = await _inner.CreateOrderAsync(order);
_logger.LogInformation("Order {OrderId} created", result.Id);
return result;
}```
}
// Registration with Scrutor
builder.Services.AddScoped<IOrderService, OrderService>();
builder.Services.Decorate<IOrderService, LoggingOrderService>();
Testing with DI
Unit Testing
Mock dependencies:
public class OrderServiceTests
{
```csharp
[Fact]
public async Task CreateOrder_ValidOrder_ReturnsCreatedOrder()
{
// Arrange
var mockRepo = new Mock<IOrderRepository>();
mockRepo.Setup(r => r.AddAsync(It.IsAny<Order>()))
.ReturnsAsync((Order o) => o);
var service = new OrderService(mockRepo.Object);
// Act
var order = new Order { CustomerId = 1, Total = 100m };
var result = await service.CreateOrderAsync(order);
// Assert
Assert.NotNull(result);
mockRepo.Verify(r => r.AddAsync(It.IsAny<Order>()), Times.Once);
}```
}
Integration Testing
WebApplicationFactory:
public class OrderApiTests : IClassFixture<WebApplicationFactory<Program>>
{
```csharp
private readonly WebApplicationFactory<Program> _factory;
public OrderApiTests(WebApplicationFactory<Program> factory)
{
_factory = factory.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
// Replace dependencies for testing
services.RemoveAll<IEmailService>();
services.AddSingleton<IEmailService, FakeEmailService>();
});
});
}
[Fact]
public async Task GetOrder_ExistingId_ReturnsOrder()
{
var client = _factory.CreateClient();
var response = await client.GetAsync("/api/orders/1");
response.EnsureSuccessStatusCode();
var order = await response.Content.ReadFromJsonAsync<Order>();
Assert.NotNull(order);
}```
}
Best Practices
- Prefer Constructor Injection: Makes dependencies explicit
- Avoid Service Locator: Don't inject IServiceProvider unless necessary
- Register Interfaces: Program to abstractions, not implementations
- Watch Lifetimes: Prevent captive dependencies
- Use Extension Methods: Organize related registrations
- Validate on Startup: Use
ValidateOnBuild()andValidateScopes() - Keep Constructors Simple: Move complex logic to factory methods
Validation
Figure: Configuration and management dashboard with status overview.
Detect configuration issues:
var builder = WebApplication.CreateBuilder(args);
builder.Host.UseDefaultServiceProvider(options =>
{
```text
options.ValidateScopes = true;
options.ValidateOnBuild = true;```
});
// Throws on startup if:
// - Captive dependencies detected
// - Missing registrations
// - Circular dependencies
Troubleshooting
Circular Dependencies:
// ❌ Circular dependency
public class ServiceA
{
```text
public ServiceA(ServiceB serviceB) { }```
}
public class ServiceB
{
```text
public ServiceB(ServiceA serviceA) { }```
}
// ✅ Break the cycle
public class ServiceA
{
```text
private ServiceB _serviceB;
public void Initialize(ServiceB serviceB)
{
_serviceB = serviceB;
}```
}
Captive Dependency Detection:
// Enable validation
builder.Host.UseDefaultServiceProvider(options =>
{
```text
options.ValidateScopes = true; // Detects captive dependencies```
});
Architecture Decision and Tradeoffs
When designing application development solutions with .NET, consider these key architectural trade-offs:
| Approach | Best For | Tradeoff |
|---|---|---|
| Managed / platform service | Rapid delivery, reduced ops burden | Less customisation, potential vendor lock-in |
| Custom / self-hosted | Full control, advanced tuning | Higher operational overhead and cost |
Recommendation: Start with the managed approach for most workloads and move to custom only when specific requirements demand it.
Validation and Versioning
- Last validated: April 2026
- Validate examples against your tenant, region, and SKU constraints before production rollout.
- Keep module, CLI, and SDK versions pinned in automation pipelines and review quarterly.
Security and Governance Considerations
- Apply least-privilege access using RBAC roles and just-in-time elevation for admin tasks.
- Store secrets in managed secret stores and avoid embedding credentials in scripts or source files.
- Enable audit logging, data protection policies, and periodic access reviews for regulated workloads.
Cost and Performance Notes
- Define budgets and alerts, then monitor usage and cost trends continuously after go-live.
- Baseline performance with synthetic and real-user checks before and after major changes.
- Scale resources with measured thresholds and revisit sizing after usage pattern changes.
Official Microsoft References
- https://learn.microsoft.com/dotnet/
- https://learn.microsoft.com/aspnet/core/
- https://learn.microsoft.com/azure/developer/dotnet/
Public Examples from Official Sources
- These examples are sourced from official public Microsoft documentation and sample repositories.
- Documentation examples: https://learn.microsoft.com/dotnet/
- Sample repositories: https://github.com/dotnet/samples
- Prefer adapting these examples to your tenant, subscriptions, and governance requirements before production use.
Key Takeaways
- Transient for stateless, Scoped for per-request, Singleton for application-wide
- Avoid captive dependencies by respecting lifetime hierarchies
- Keyed services enable multiple implementations with clean resolution
- Factory patterns provide control over instance creation
- Extension methods organize registrations and improve maintainability
Next Steps
- Explore third-party containers (Autofac, Lamar) for advanced scenarios
- Implement property injection for optional dependencies
- Learn interceptors for cross-cutting concerns (logging, caching)
- Use Scrutor for assembly scanning and decoration
Additional Resources
Inject dependencies, not problems.
Discussion