Home / .NET / Dependency Injection in ASP.NET Core: Mastering Service Lifetimes and Patterns
.NET

Dependency Injection in ASP.NET Core: Mastering Service Lifetimes and Patterns

Dependency Injection (DI) is fundamental to building testable, maintainable ASP.NET Core applications.

What you will learn

Practical execution with concise explanations, real implementation patterns, and production-ready recommendations.

Dependency Injection in ASP.NET Core: Mastering Service Lifetimes and Patterns

_cache[key] = value; // Thread-safe }

public T Get(string key) { return _cache.TryGetValue(key, out var value) ? (T)value : default; }``` }


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

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

  1. Prefer Constructor Injection: Makes dependencies explicit
  2. Avoid Service Locator: Don't inject IServiceProvider unless necessary
  3. Register Interfaces: Program to abstractions, not implementations
  4. Watch Lifetimes: Prevent captive dependencies
  5. Use Extension Methods: Organize related registrations
  6. Validate on Startup: Use ValidateOnBuild() and ValidateScopes()
  7. Keep Constructors Simple: Move complex logic to factory methods

Validation

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