Post

C# Dependency Injection and IoC Containers Complete Guide

Introduction to Dependency Injection

Dependency Injection (DI) is a design pattern that implements Inversion of Control (IoC) for resolving dependencies. It’s a fundamental technique for writing maintainable, testable, and loosely coupled code. In modern C# development, especially with ASP.NET Core, DI has become a core part of application architecture.

This guide covers DI concepts, IoC containers, implementation patterns, best practices, and advanced scenarios.

Understanding Dependency Injection

What is Dependency Injection?

Dependency Injection is a technique where an object receives its dependencies from external sources rather than creating them itself.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// WITHOUT Dependency Injection
public class OrderService
{
    private readonly OrderRepository _repository;
    
    public OrderService()
    {
        // Tight coupling - creates its own dependency
        _repository = new OrderRepository();
    }
    
    public void CreateOrder(Order order)
    {
        _repository.Save(order);
    }
}

// WITH Dependency Injection
public class OrderService
{
    private readonly IOrderRepository _repository;
    
    // Dependency injected via constructor
    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }
    
    public void CreateOrder(Order order)
    {
        _repository.Save(order);
    }
}

Benefits of Dependency Injection

1. Loose Coupling: Components depend on abstractions, not concrete implementations.

2. Testability: Easy to mock dependencies in unit tests.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[TestMethod]
public void CreateOrder_SavesOrderSuccessfully()
{
    // Arrange
    var mockRepository = new Mock<IOrderRepository>();
    var service = new OrderService(mockRepository.Object);
    var order = new Order { Id = 1 };
    
    // Act
    service.CreateOrder(order);
    
    // Assert
    mockRepository.Verify(r => r.Save(order), Times.Once);
}

3. Maintainability: Changes to dependencies don’t require changes to dependent classes.

4. Flexibility: Easy to swap implementations without modifying code.

Types of Dependency Injection

1. Constructor Injection

The most common and recommended approach.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ProductService
{
    private readonly IProductRepository _repository;
    private readonly ILogger<ProductService> _logger;
    
    // Dependencies injected via constructor
    public ProductService(
        IProductRepository repository,
        ILogger<ProductService> logger)
    {
        _repository = repository ?? throw new ArgumentNullException(nameof(repository));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }
    
    public async Task<Product> GetProductAsync(int id)
    {
        _logger.LogInformation("Fetching product {ProductId}", id);
        return await _repository.GetByIdAsync(id);
    }
}

Advantages:

  • Dependencies are explicit and required
  • Immutability (readonly fields)
  • Easy to test
  • Prevents partially constructed objects

2. Property Injection

Used for optional dependencies.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class EmailService
{
    private readonly IEmailSender _sender;
    
    public EmailService(IEmailSender sender)
    {
        _sender = sender;
    }
    
    // Optional dependency via property
    public ILogger Logger { get; set; }
    
    public async Task SendEmailAsync(string to, string subject, string body)
    {
        Logger?.LogInformation("Sending email to {Recipient}", to);
        await _sender.SendAsync(to, subject, body);
    }
}

Disadvantages:

  • Dependencies not immediately obvious
  • Can be set to null
  • Less preferred than constructor injection

3. Method Injection

Dependencies passed as method parameters.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ReportGenerator
{
    public Report Generate(IDataSource dataSource, IReportTemplate template)
    {
        var data = dataSource.GetData();
        return template.Format(data);
    }
}

// Usage
var generator = new ReportGenerator();
var report = generator.Generate(
    new DatabaseDataSource(),
    new PdfTemplate()
);

Use cases:

  • Different dependency needed per method call
  • Dependency not needed for entire object lifetime

IoC Containers in .NET

Built-in Microsoft.Extensions.DependencyInjection

ASP.NET Core includes a built-in DI container.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Startup.cs or Program.cs
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Transient - new instance every time
        services.AddTransient<IEmailService, EmailService>();
        
        // Scoped - one instance per request
        services.AddScoped<IOrderService, OrderService>();
        
        // Singleton - one instance for application lifetime
        services.AddSingleton<IConfiguration, Configuration>();
        
        // Register with factory
        services.AddTransient<IProductService>(provider =>
        {
            var repository = provider.GetRequiredService<IProductRepository>();
            var cache = provider.GetService<ICache>();
            return new ProductService(repository, cache);
        });
    }
}

Service Lifetimes

Transient:

  • Created each time they’re requested
  • Best for lightweight, stateless services
  • Each request gets a new instance
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
services.AddTransient<IEmailSender, SmtpEmailSender>();

// Usage
public class Controller
{
    private readonly IEmailSender _sender1;
    private readonly IEmailSender _sender2;
    
    public Controller(IEmailSender sender1, IEmailSender sender2)
    {
        _sender1 = sender1;  // New instance
        _sender2 = sender2;  // Another new instance
        // sender1 != sender2
    }
}

Scoped:

  • Created once per client request (scope)
  • Best for per-request operations
  • Same instance within a request
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
services.AddScoped<IOrderRepository, OrderRepository>();

// In web request:
// All services within the same HTTP request share the same instance
public class OrderController
{
    private readonly IOrderRepository _repo;
    
    public OrderController(IOrderRepository repo)
    {
        _repo = repo;  // Same instance as injected in OrderService
    }
}

public class OrderService
{
    private readonly IOrderRepository _repo;
    
    public OrderService(IOrderRepository repo)
    {
        _repo = repo;  // Same instance as in OrderController
    }
}

Singleton:

  • Created once and shared throughout application lifetime
  • Best for stateless services, configurations, caches
  • Must be thread-safe
1
2
3
4
5
6
7
8
9
10
11
12
services.AddSingleton<ICache, MemoryCache>();

// All requests share the same instance
public class Service1
{
    public Service1(ICache cache) { }  // Same instance
}

public class Service2
{
    public Service2(ICache cache) { }  // Same instance
}

Lifetime Best Practices

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class LifetimeExamples
{
    public void ConfigureServices(IServiceCollection services)
    {
        // GOOD - Stateless service as Transient
        services.AddTransient<IEmailValidator, EmailValidator>();
        
        // GOOD - Per-request state as Scoped
        services.AddScoped<IShoppingCart, ShoppingCart>();
        
        // GOOD - Shared state as Singleton
        services.AddSingleton<IConfigurationProvider, ConfigurationProvider>();
        
        // BAD - Don't inject Scoped into Singleton
        // services.AddSingleton<ServiceWithScopedDependency>();
        
        // GOOD - Use IServiceScopeFactory in Singleton
        services.AddSingleton<BackgroundService>();
        services.AddScoped<IScopedService, ScopedService>();
    }
}

public class BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    
    public BackgroundService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }
    
    public async Task DoWorkAsync()
    {
        using var scope = _scopeFactory.CreateScope();
        var scopedService = scope.ServiceProvider
            .GetRequiredService<IScopedService>();
        
        await scopedService.ProcessAsync();
    }
}

Advanced DI Patterns

1. Factory Pattern with DI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public interface INotificationFactory
{
    INotification CreateNotification(NotificationType type);
}

public class NotificationFactory : INotificationFactory
{
    private readonly IServiceProvider _serviceProvider;
    
    public NotificationFactory(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    public INotification CreateNotification(NotificationType type)
    {
        return type switch
        {
            NotificationType.Email => 
                _serviceProvider.GetRequiredService<EmailNotification>(),
            NotificationType.Sms => 
                _serviceProvider.GetRequiredService<SmsNotification>(),
            NotificationType.Push => 
                _serviceProvider.GetRequiredService<PushNotification>(),
            _ => throw new ArgumentException("Invalid notification type")
        };
    }
}

// Registration
services.AddTransient<EmailNotification>();
services.AddTransient<SmsNotification>();
services.AddTransient<PushNotification>();
services.AddSingleton<INotificationFactory, NotificationFactory>();

2. Decorator Pattern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public interface IOrderService
{
    Task<Order> CreateOrderAsync(Order order);
}

public class OrderService : IOrderService
{
    private readonly IOrderRepository _repository;
    
    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }
    
    public async Task<Order> CreateOrderAsync(Order order)
    {
        return await _repository.SaveAsync(order);
    }
}

// Decorator for logging
public class LoggingOrderService : IOrderService
{
    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);
        
        try
        {
            var result = await _inner.CreateOrderAsync(order);
            _logger.LogInformation("Order {OrderId} created successfully", order.Id);
            return result;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to create order {OrderId}", order.Id);
            throw;
        }
    }
}

// Registration with Scrutor
services.AddScoped<IOrderService, OrderService>();
services.Decorate<IOrderService, LoggingOrderService>();

3. Named Services

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public interface IPaymentProcessor
{
    Task<PaymentResult> ProcessAsync(Payment payment);
}

public class PayPalProcessor : IPaymentProcessor
{
    public async Task<PaymentResult> ProcessAsync(Payment payment)
    {
        // PayPal implementation
        return await Task.FromResult(new PaymentResult());
    }
}

public class StripeProcessor : IPaymentProcessor
{
    public async Task<PaymentResult> ProcessAsync(Payment payment)
    {
        // Stripe implementation
        return await Task.FromResult(new PaymentResult());
    }
}

// Named service resolver
public interface IPaymentProcessorResolver
{
    IPaymentProcessor Resolve(string provider);
}

public class PaymentProcessorResolver : IPaymentProcessorResolver
{
    private readonly IServiceProvider _serviceProvider;
    
    public PaymentProcessorResolver(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
    
    public IPaymentProcessor Resolve(string provider)
    {
        return provider.ToLower() switch
        {
            "paypal" => _serviceProvider.GetRequiredService<PayPalProcessor>(),
            "stripe" => _serviceProvider.GetRequiredService<StripeProcessor>(),
            _ => throw new ArgumentException($"Unknown provider: {provider}")
        };
    }
}

// Registration
services.AddScoped<PayPalProcessor>();
services.AddScoped<StripeProcessor>();
services.AddScoped<IPaymentProcessorResolver, PaymentProcessorResolver>();

4. Options Pattern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class EmailSettings
{
    public string SmtpServer { get; set; }
    public int Port { get; set; }
    public string Username { get; set; }
    public string Password { get; set; }
}

// appsettings.json
{
  "EmailSettings": {
    "SmtpServer": "smtp.example.com",
    "Port": 587,
    "Username": "user@example.com",
    "Password": "secret"
  }
}

// Startup
services.Configure<EmailSettings>(
    Configuration.GetSection("EmailSettings"));

// Service using options
public class EmailService
{
    private readonly EmailSettings _settings;
    
    public EmailService(IOptions<EmailSettings> options)
    {
        _settings = options.Value;
    }
    
    public async Task SendAsync(string to, string subject, string body)
    {
        // Use _settings.SmtpServer, _settings.Port, etc.
    }
}

1. Autofac

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using Autofac;
using Autofac.Extensions.DependencyInjection;

public class Startup
{
    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        services.AddControllers();
        
        var builder = new ContainerBuilder();
        builder.Populate(services);
        
        // Register types
        builder.RegisterType<OrderService>()
            .As<IOrderService>()
            .InstancePerLifetimeScope();
        
        // Register with parameters
        builder.RegisterType<ProductService>()
            .As<IProductService>()
            .WithParameter("timeout", TimeSpan.FromSeconds(30));
        
        // Register modules
        builder.RegisterModule<RepositoryModule>();
        
        var container = builder.Build();
        return new AutofacServiceProvider(container);
    }
}

// Module for organizing registrations
public class RepositoryModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterType<OrderRepository>()
            .As<IOrderRepository>()
            .InstancePerLifetimeScope();
        
        builder.RegisterType<ProductRepository>()
            .As<IProductRepository>()
            .InstancePerLifetimeScope();
    }
}

2. Ninject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using Ninject;

public class Program
{
    private static IKernel ConfigureServices()
    {
        var kernel = new StandardKernel();
        
        // Bind interface to implementation
        kernel.Bind<IOrderService>().To<OrderService>();
        
        // Bind with scope
        kernel.Bind<IOrderRepository>().To<OrderRepository>()
            .InRequestScope();
        
        // Bind singleton
        kernel.Bind<IConfiguration>().To<Configuration>()
            .InSingletonScope();
        
        // Bind with factory
        kernel.Bind<IProductService>().ToMethod(ctx =>
        {
            var repo = ctx.Kernel.Get<IProductRepository>();
            return new ProductService(repo);
        });
        
        return kernel;
    }
    
    public static void Main()
    {
        var kernel = ConfigureServices();
        var service = kernel.Get<IOrderService>();
    }
}

3. Castle Windsor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
using Castle.MicroKernel.Registration;
using Castle.Windsor;

public class Program
{
    private static IWindsorContainer ConfigureServices()
    {
        var container = new WindsorContainer();
        
        // Register components
        container.Register(
            Component.For<IOrderService>()
                .ImplementedBy<OrderService>()
                .LifestyleScoped()
        );
        
        container.Register(
            Component.For<IProductService>()
                .ImplementedBy<ProductService>()
                .LifestyleTransient()
        );
        
        // Register with interceptors
        container.Register(
            Component.For<IOrderRepository>()
                .ImplementedBy<OrderRepository>()
                .Interceptors<LoggingInterceptor>()
        );
        
        return container;
    }
    
    public static void Main()
    {
        var container = ConfigureServices();
        var service = container.Resolve<IOrderService>();
    }
}

Testing with DI

Unit Testing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class OrderServiceTests
{
    [Fact]
    public async Task CreateOrder_ValidOrder_ReturnsSuccess()
    {
        // Arrange
        var mockRepository = new Mock<IOrderRepository>();
        var mockLogger = new Mock<ILogger<OrderService>>();
        
        mockRepository
            .Setup(r => r.SaveAsync(It.IsAny<Order>()))
            .ReturnsAsync(new Order { Id = 1 });
        
        var service = new OrderService(
            mockRepository.Object,
            mockLogger.Object
        );
        
        var order = new Order { CustomerId = 1, Total = 100 };
        
        // Act
        var result = await service.CreateOrderAsync(order);
        
        // Assert
        Assert.NotNull(result);
        Assert.Equal(1, result.Id);
        mockRepository.Verify(r => r.SaveAsync(order), Times.Once);
    }
}

Integration Testing

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class OrderServiceIntegrationTests : IClassFixture<WebApplicationFactory<Startup>>
{
    private readonly WebApplicationFactory<Startup> _factory;
    
    public OrderServiceIntegrationTests(WebApplicationFactory<Startup> factory)
    {
        _factory = factory;
    }
    
    [Fact]
    public async Task CreateOrder_IntegrationTest()
    {
        // Arrange
        var client = _factory.CreateClient();
        var order = new Order { CustomerId = 1, Total = 100 };
        
        // Act
        var response = await client.PostAsJsonAsync("/api/orders", order);
        
        // Assert
        response.EnsureSuccessStatusCode();
        var result = await response.Content.ReadFromJsonAsync<Order>();
        Assert.NotNull(result);
        Assert.True(result.Id > 0);
    }
}

Common Pitfalls and Best Practices

1. Service Locator Anti-Pattern

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// BAD - Service Locator (anti-pattern)
public class OrderService
{
    public void CreateOrder(Order order)
    {
        var repository = ServiceLocator.GetService<IOrderRepository>();
        repository.Save(order);
    }
}

// GOOD - Constructor Injection
public class OrderService
{
    private readonly IOrderRepository _repository;
    
    public OrderService(IOrderRepository repository)
    {
        _repository = repository;
    }
    
    public void CreateOrder(Order order)
    {
        _repository.Save(order);
    }
}

2. Captive Dependencies

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// BAD - Singleton capturing Scoped dependency
public class SingletonService
{
    private readonly IScopedService _scopedService;
    
    // This keeps the scoped service alive forever!
    public SingletonService(IScopedService scopedService)
    {
        _scopedService = scopedService;
    }
}

// GOOD - Use IServiceScopeFactory
public class SingletonService
{
    private readonly IServiceScopeFactory _scopeFactory;
    
    public SingletonService(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }
    
    public void DoWork()
    {
        using var scope = _scopeFactory.CreateScope();
        var scopedService = scope.ServiceProvider
            .GetRequiredService<IScopedService>();
        scopedService.Process();
    }
}

3. Over-Injection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// BAD - Too many dependencies
public class OrderService
{
    public OrderService(
        IOrderRepository repo,
        ICustomerService customerService,
        IInventoryService inventoryService,
        IPaymentService paymentService,
        IShippingService shippingService,
        INotificationService notificationService,
        ILogger logger,
        IConfiguration config)
    {
        // Too many dependencies suggests class does too much
    }
}

// GOOD - Extract to smaller services
public class OrderService
{
    private readonly IOrderRepository _repository;
    private readonly IOrderProcessor _processor;
    private readonly ILogger<OrderService> _logger;
    
    public OrderService(
        IOrderRepository repository,
        IOrderProcessor processor,
        ILogger<OrderService> logger)
    {
        _repository = repository;
        _processor = processor;
        _logger = logger;
    }
}

public class OrderProcessor
{
    // Handles complex order processing logic
    public OrderProcessor(
        IPaymentService paymentService,
        IInventoryService inventoryService,
        INotificationService notificationService)
    {
        // Focused responsibility
    }
}

4. Null Checking

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// GOOD - Always validate constructor parameters
public class OrderService
{
    private readonly IOrderRepository _repository;
    private readonly ILogger<OrderService> _logger;
    
    public OrderService(
        IOrderRepository repository,
        ILogger<OrderService> logger)
    {
        _repository = repository 
            ?? throw new ArgumentNullException(nameof(repository));
        _logger = logger 
            ?? throw new ArgumentNullException(nameof(logger));
    }
}

Performance Considerations

1. Avoid Unnecessary Transient Services

1
2
3
4
5
6
7
// BAD - Expensive transient service
services.AddTransient<ExpensiveService>();

// GOOD - Use appropriate lifetime
services.AddSingleton<ExpensiveService>();
// or
services.AddScoped<ExpensiveService>();

2. Lazy Initialization

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class ServiceWithLazyDependency
{
    private readonly Lazy<IExpensiveService> _expensiveService;
    
    public ServiceWithLazyDependency(Lazy<IExpensiveService> expensiveService)
    {
        _expensiveService = expensiveService;
    }
    
    public void DoWork()
    {
        if (SomeCondition())
        {
            // Service only created if needed
            _expensiveService.Value.Process();
        }
    }
}

// Registration
services.AddTransient<IExpensiveService, ExpensiveService>();
services.AddTransient(provider => 
    new Lazy<IExpensiveService>(
        () => provider.GetRequiredService<IExpensiveService>()
    )
);

Conclusion

Dependency Injection and IoC containers are fundamental to modern C# development. Key takeaways include:

  • Use constructor injection for required dependencies
  • Understand service lifetimes (Transient, Scoped, Singleton)
  • Avoid service locator anti-pattern
  • Be careful with captive dependencies
  • Keep constructors simple and avoid over-injection
  • Choose the right IoC container for your needs
  • Always validate constructor parameters
  • Use factory patterns for complex object creation
  • Leverage the Options pattern for configuration
  • Write testable code with proper abstractions

By mastering DI and IoC, you can build maintainable, testable, and flexible applications that follow SOLID principles.

References

This post is licensed under CC BY 4.0 by the author.