Isaac.

Dependency Injection in .NET

Master dependency injection for loosely-coupled, testable code.

By EMEPublished: February 20, 2025
dependency injectiondicsharpdesign patternsiocdotnet

A Simple Analogy

Dependency injection is like room service. Instead of maintaining your own kitchen, you call room service and they bring what you need. Your room doesn't depend on knowing how to cook; it depends on receiving meals. Your code doesn't create dependencies; it receives them.


What Is Dependency Injection?

Dependency Injection (DI) is a design pattern where classes receive their dependencies rather than creating them. It decouples components and enables easier testing and maintenance.


Why Use DI?

  • Loose coupling: Classes depend on abstractions, not concrete types
  • Testability: Mock dependencies easily
  • Flexibility: Swap implementations without changing code
  • Maintainability: Dependencies clear and centralized
  • Reusability: Components work with any compatible dependency

Before DI (Tightly Coupled)

public class OrderService
{
    private readonly EmailNotifier _emailer;
    
    public OrderService()
    {
        _emailer = new EmailNotifier();  // Creates own dependency
    }
    
    public void CreateOrder(Order order)
    {
        // Process order
        _emailer.Send("Order confirmed");
    }
}

// Problems: Hard to test, can't swap EmailNotifier

After DI (Loosely Coupled)

public interface IEmailNotifier
{
    Task SendAsync(string message);
}

public class OrderService
{
    private readonly IEmailNotifier _notifier;
    
    // Dependency injected via constructor
    public OrderService(IEmailNotifier notifier)
    {
        _notifier = notifier;
    }
    
    public async Task CreateOrderAsync(Order order)
    {
        // Process order
        await _notifier.SendAsync("Order confirmed");
    }
}

// Can easily test with mock
public class MockNotifier : IEmailNotifier
{
    public Task SendAsync(string msg) => Task.CompletedTask;
}

ASP.NET Core Setup

var builder = WebApplication.CreateBuilder(args);

// Register dependencies
builder.Services.AddScoped<IOrderRepository, OrderRepository>();
builder.Services.AddScoped<IEmailNotifier, SmtpEmailNotifier>();
builder.Services.AddScoped<OrderService>();

var app = builder.Build();

// Automatically injected in controllers
[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    private readonly OrderService _orderService;
    
    // DI container provides dependencies
    public OrdersController(OrderService orderService)
    {
        _orderService = orderService;
    }
}

Lifetimes

| Lifetime | Behavior | |----------|----------| | Transient | New instance every time | | Scoped | One per request/scope | | Singleton | One for entire app lifetime |

builder.Services.AddTransient<ILogger, Logger>();
builder.Services.AddScoped<IRepository, Repository>();
builder.Services.AddSingleton<ICache, MemoryCache>();

Related Concepts to Explore

  • Service locator pattern (anti-pattern)
  • IoC containers and service registration
  • Constructor injection vs. property injection
  • Factory pattern with DI

Summary

Dependency injection decouples components, improves testability, and makes code more flexible. Master it to build maintainable, professional-grade applications.