C# Dependency Injection and IoC Containers Complete Guide

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 couple

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#

Contents