Skip to main content

Dependency Injection

Dependency Injection (DI) is a design pattern that implements Inversion of Control (IoC) for managing dependencies between objects. Rather than objects creating their own dependencies, they receive them from an external source. This promotes loose coupling, testability, and maintainable code.

Understanding Dependency Injection

The Restaurant Analogy

Imagine you're running a restaurant. Without DI, your chef would need to:

  • Grow vegetables in a garden
  • Raise chickens for eggs
  • Mill flour for bread
  • Manufacture cooking equipment

This is tightly coupled and impractical. With DI, you provide the chef with pre-sourced ingredients and equipment. The chef doesn't care where the ingredients come from—they just use what's provided to create dishes.

In code terms:

  • Without DI: Classes create their own dependencies (the chef grows vegetables)
  • With DI: Classes receive dependencies from outside (ingredients are provided)

Inversion of Control

Traditional programming flow: "I need a database connection, so I'll create one."

public class PlatformsController
{
private readonly AppDbContext _context;

public PlatformsController()
{
_context = new AppDbContext(); // ❌ Tightly coupled
}
}

Inversion of Control: "I need a database connection, someone else will provide it."

public class PlatformsController
{
private readonly AppDbContext _context;

public PlatformsController(AppDbContext context) // ✅ Dependency injected
{
_context = context;
}
}

Control is inverted: The controller no longer controls how its dependencies are created—that responsibility is inverted to an external container.

The .NET DI Container

ASP.NET Core includes a built-in DI container (also called the service container) that manages object creation and lifetime. It's lightweight, capable, and sufficient for most applications.

The container:

  • Resolves dependencies: Creates objects and their dependencies automatically
  • Manages lifetimes: Controls when objects are created and disposed
  • Handles dependency graphs: Resolves nested dependencies recursively

When your application starts, services are registered in the container. When a class requests a dependency, the container automatically provides an instance.

Request → Controller needs IRepository 
→ Container creates/provides IRepository instance
→ Controller uses IRepository

Registering Services

Services are registered in Program.cs using extension methods on IServiceCollection. This tells the container what to provide when a dependency is requested.

Basic Registration

var builder = WebApplication.CreateBuilder(args);

// Register services
builder.Services.AddScoped<ICommandRepository, CommandRepository>();
builder.Services.AddSingleton<IConfigService, ConfigService>();
builder.Services.AddTransient<IEmailService, EmailService>();

Registration Patterns

Interface to Implementation:

builder.Services.AddScoped<IPlatformRepository, PlatformRepository>();
// When someone requests IPlatformRepository, provide PlatformRepository

Concrete Types:

builder.Services.AddScoped<AppDbContext>();
// Register the concrete type directly

Factory Functions:

builder.Services.AddScoped<IApiClient>(provider => 
{
var config = provider.GetRequiredService<IConfiguration>();
var apiKey = config["ApiKeys:External"];
return new ApiClient(apiKey);
});

Service Lifetimes

The container manages three distinct lifetimes that control when instances are created and disposed.

Transient

Created every time they're requested.

builder.Services.AddTransient<IEmailService, EmailService>();

Characteristics:

  • New instance per request
  • Disposed after use
  • Lightweight services with no state
  • No shared data between requests

Use for:

  • Stateless services
  • Lightweight operations
  • Services that shouldn't be shared

Example:

public class EmailService : IEmailService
{
public Task SendEmail(string to, string subject, string body)
{
// Send email logic
return Task.CompletedTask;
}
}

Scoped

Created once per HTTP request (or scope).

builder.Services.AddScoped<ICommandRepository, CommandRepository>();
builder.Services.AddScoped<AppDbContext>();

Characteristics:

  • One instance per HTTP request
  • Shared across the request pipeline
  • Disposed at end of request
  • Most common lifetime for web applications

Use for:

  • Database contexts (EF Core)
  • Repositories
  • Unit of Work patterns
  • Request-specific services

Example:

public class CommandRepository : ICommandRepository
{
private readonly AppDbContext _context;

public CommandRepository(AppDbContext context)
{
_context = context; // Same context throughout the request
}
}

Singleton

Created once for the application lifetime.

builder.Services.AddSingleton<ICacheService, CacheService>();

Characteristics:

  • Single instance for entire application
  • Created on first request (or at startup)
  • Never disposed until application shuts down
  • Shared across all requests and users

Use for:

  • Configuration services
  • Caching services
  • Thread-safe stateless services
  • Application-wide services

Example:

public class CacheService : ICacheService
{
private readonly ConcurrentDictionary<string, object> _cache = new();

public void Set(string key, object value) => _cache[key] = value;
public object? Get(string key) => _cache.TryGetValue(key, out var value) ? value : null;
}

Lifetime Comparison

LifetimeCreatedSharedDisposedUse Case
TransientEvery injectionNoAfter useLightweight, stateless services
ScopedPer HTTP requestWithin requestEnd of requestDatabase contexts, repositories
SingletonOnceAcross entire appApplication shutdownCaches, configuration, stateless utilities

Lifetime Best Practices

Safe:

  • Singleton → Singleton ✅
  • Singleton → Transient ✅
  • Scoped → Scoped ✅
  • Scoped → Transient ✅
  • Transient → Transient ✅

Unsafe (Captive Dependencies):

  • Singleton → Scoped ❌ (Scoped service becomes singleton)
  • Singleton → Transient ⚠️ (Transient service becomes singleton)
  • Scoped → Singleton ✅ (But singleton must be thread-safe)

Injecting Dependencies

Dependencies are injected through constructors. The DI container automatically provides the required instances.

Constructor Injection

public class PlatformsController : ControllerBase
{
private readonly IPlatformRepository _repository;
private readonly ILogger<PlatformsController> _logger;
private readonly IMapper _mapper;

// DI container injects all dependencies
public PlatformsController(
IPlatformRepository repository,
ILogger<PlatformsController> logger,
IMapper mapper)
{
_repository = repository;
_logger = logger;
_mapper = mapper;
}

[HttpGet]
public async Task<ActionResult<IEnumerable<PlatformReadDto>>> GetPlatforms()
{
_logger.LogInformation("Retrieving all platforms");
var platforms = await _repository.GetAllAsync();
return Ok(_mapper.Map<IEnumerable<PlatformReadDto>>(platforms));
}
}

Multiple Dependencies

The container resolves entire dependency graphs:

// Service A depends on B and C
public class ServiceA
{
public ServiceA(IServiceB serviceB, IServiceC serviceC) { }
}

// Service B depends on D
public class ServiceB : IServiceB
{
public ServiceB(IServiceD serviceD) { }
}

// When ServiceA is requested:
// Container creates ServiceD → ServiceB → ServiceC → ServiceA

Resolving Services Manually

Rarely needed, but possible using IServiceProvider:

var app = builder.Build();

using (var scope = app.Services.CreateScope())
{
var repository = scope.ServiceProvider.GetRequiredService<IPlatformRepository>();
// Use repository
}

Benefits of Dependency Injection

Testability: Mock dependencies easily for unit testing

// Test with mock repository
var mockRepo = new Mock<IPlatformRepository>();
var controller = new PlatformsController(mockRepo.Object, ...);
  • Loose Coupling: Classes depend on abstractions, not concrete implementations
  • Maintainability: Change implementations without modifying consumers
  • Flexibility: Swap implementations for different environments (e.g., mock services in development)
  • Lifecycle Management: Container handles creation and disposal automatically

Common Patterns

Repository Pattern with DI

// Register
builder.Services.AddScoped<IPlatformRepository, PlatformRepository>();
builder.Services.AddScoped<ICommandRepository, CommandRepository>();

// Inject
public class PlatformsController : ControllerBase
{
private readonly IPlatformRepository _repository;

public PlatformsController(IPlatformRepository repository)
{
_repository = repository;
}
}

Service Layer Pattern

// Register
builder.Services.AddScoped<IPlatformService, PlatformService>();

// Service layer
public class PlatformService : IPlatformService
{
private readonly IPlatformRepository _repository;
private readonly IMapper _mapper;

public PlatformService(IPlatformRepository repository, IMapper mapper)
{
_repository = repository;
_mapper = mapper;
}
}

// Controller
public class PlatformsController : ControllerBase
{
private readonly IPlatformService _service;

public PlatformsController(IPlatformService service)
{
_service = service;
}
}

Dependency Injection is fundamental to modern .NET development. It's the backbone of ASP.NET Core's architecture and enables clean, testable, maintainable applications. Once you understand DI, you'll wonder how you ever coded without it.


Further Reading: