C# Dependency Injection
Introduction
Dependency Injection (DI) is a design pattern that implements Inversion of Control (IoC) for resolving dependencies between objects. In simpler terms, it's a technique that allows you to supply the dependencies of a class from the outside rather than having the class create them internally.
In C# web development, particularly in ASP.NET Core applications, dependency injection is built into the framework and plays a crucial role in creating maintainable, testable, and scalable applications.
By the end of this tutorial, you'll understand:
- What dependency injection is and why it's important
- How to implement DI in C# applications
- How the built-in DI container works in ASP.NET Core
- Common patterns and best practices for DI
What is Dependency Injection?
Imagine you're building a web application that needs to access a database. Traditionally, you might create the database connection directly within your classes:
public class CustomerService
{
    private readonly DatabaseConnection _database;
    public CustomerService()
    {
        _database = new DatabaseConnection("connection-string");
    }
    public Customer GetCustomer(int id)
    {
        return _database.Find<Customer>(id);
    }
}
This approach creates tight coupling between CustomerService and DatabaseConnection. If you want to:
- Test CustomerServicewith a mock database
- Change the database implementation
- Reuse CustomerServicewith different connection strings
...you'd need to modify the CustomerService class directly.
Dependency injection solves this by providing dependencies from the outside:
public class CustomerService
{
    private readonly IDatabaseConnection _database;
    public CustomerService(IDatabaseConnection database)
    {
        _database = database;
    }
    public Customer GetCustomer(int id)
    {
        return _database.Find<Customer>(id);
    }
}
Now CustomerService doesn't create its dependency; it receives it through its constructor. This is called constructor injection, the most common form of DI.
Why Use Dependency Injection?
- Loose coupling: Classes are no longer responsible for creating their dependencies
- Testability: You can easily provide mock implementations during tests
- Flexibility: Dependencies can be swapped without changing the dependent classes
- Separation of concerns: Classes focus on their core responsibilities
- Lifetime management: The DI container can manage object lifecycles
The DI Container in ASP.NET Core
ASP.NET Core includes a built-in dependency injection container that handles:
- Registration of services
- Resolution of dependencies
- Management of object lifecycles
Let's see how to use it:
Step 1: Register Services
Services are registered in the Startup.cs file (or Program.cs in newer versions) within the ConfigureServices method:
public void ConfigureServices(IServiceCollection services)
{
    // Register interfaces and their implementations
    services.AddScoped<ICustomerRepository, CustomerRepository>();
    services.AddScoped<ICustomerService, CustomerService>();
    
    services.AddControllers();
}
Step 2: Consume the Services
The container automatically injects registered services into constructors:
public class CustomersController : ControllerBase
{
    private readonly ICustomerService _customerService;
    public CustomersController(ICustomerService customerService)
    {
        _customerService = customerService;
    }
    [HttpGet("{id}")]
    public ActionResult<Customer> GetById(int id)
    {
        return _customerService.GetCustomer(id);
    }
}
Service Lifetimes
ASP.NET Core DI offers three service lifetimes:
- Transient: Created each time they're requested
- Scoped: Created once per client request
- Singleton: Created the first time they're requested and reused for the application's lifetime
// Register with different lifetimes
services.AddTransient<IExampleTransient, ExampleTransient>();
services.AddScoped<IExampleScoped, ExampleScoped>();
services.AddSingleton<IExampleSingleton, ExampleSingleton>();
Choose the appropriate lifetime based on your service's characteristics:
- Transient: For lightweight, stateless services
- Scoped: For services that maintain state during a request
- Singleton: For services that maintain state across the application
Practical Example: Building a Weather Forecast Service
Let's build a small weather forecast application using dependency injection. First, we'll define our interfaces and models:
public class WeatherForecast
{
    public DateTime Date { get; set; }
    public int TemperatureC { get; set; }
    public string Summary { get; set; }
}
public interface IWeatherService
{
    IEnumerable<WeatherForecast> GetForecast();
}
Now, let's implement the service:
public class WeatherService : IWeatherService
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild",
        "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };
    public IEnumerable<WeatherForecast> GetForecast()
    {
        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = rng.Next(-20, 55),
            Summary = Summaries[rng.Next(Summaries.Length)]
        })
        .ToArray();
    }
}
In our Program.cs file (ASP.NET Core 6+):
var builder = WebApplication.CreateBuilder(args);
// Register services
builder.Services.AddControllers();
builder.Services.AddScoped<IWeatherService, WeatherService>();
var app = builder.Build();
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();
Finally, in our controller:
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private readonly IWeatherService _weatherService;
    public WeatherForecastController(IWeatherService weatherService)
    {
        _weatherService = weatherService;
    }
    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        return _weatherService.GetForecast();
    }
}
When a client requests weather data, ASP.NET Core:
- Creates a WeatherForecastControllerinstance
- Identifies that it needs an IWeatherServiceimplementation
- Creates a WeatherService(following the scoped lifetime)
- Injects it into the controller's constructor
- Calls the Getmethod, which uses the injected service
Advanced Dependency Injection
Multiple Implementations of an Interface
You can register multiple implementations of an interface and retrieve them all using IEnumerable<T>:
// Register multiple implementations
services.AddScoped<INotificationService, EmailNotificationService>();
services.AddScoped<INotificationService, SmsNotificationService>();
// In a controller or service:
public class NotificationController
{
    private readonly IEnumerable<INotificationService> _notificationServices;
    public NotificationController(IEnumerable<INotificationService> notificationServices)
    {
        _notificationServices = notificationServices;
    }
    public void NotifyAll(string message)
    {
        foreach (var service in _notificationServices)
        {
            service.Send(message);
        }
    }
}
Factory Pattern with DI
For more complex scenarios, you might need to create services with runtime parameters:
services.AddScoped<IDbConnectionFactory>(provider => 
    new DbConnectionFactory(Configuration.GetConnectionString("DefaultConnection")));
Using Third-Party DI Containers
While ASP.NET Core's built-in DI container is sufficient for most applications, you can integrate third-party containers like Autofac, Unity, or Ninject for advanced features:
// Example using Autofac
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    
    // Create Autofac container builder
    var builder = new ContainerBuilder();
    
    // Register services
    builder.RegisterType<CustomerRepository>().As<ICustomerRepository>();
    builder.RegisterType<CustomerService>().As<ICustomerService>();
    
    // Build the container and use it as the application's service provider
    var container = builder.Build();
    return new AutofacServiceProvider(container);
}
Testing with Dependency Injection
Dependency injection makes testing much more straightforward. Consider our weather service example:
public class WeatherForecastControllerTests
{
    [Fact]
    public void Get_ReturnsCorrectForecast()
    {
        // Arrange
        var mockService = new Mock<IWeatherService>();
        var testData = new List<WeatherForecast> 
        {
            new WeatherForecast 
            { 
                Date = DateTime.Today,
                TemperatureC = 20,
                Summary = "Mild" 
            }
        };
        
        mockService.Setup(service => service.GetForecast()).Returns(testData);
        
        var controller = new WeatherForecastController(mockService.Object);
        
        // Act
        var result = controller.Get();
        
        // Assert
        Assert.Equal(testData, result);
    }
}
With DI, we can easily:
- Create mock implementations of dependencies
- Test components in isolation
- Verify that components use their dependencies correctly
Common Pitfalls and Best Practices
Pitfalls to Avoid
- Service Locator Anti-Pattern: Don't use the DI container directly in your code to locate services
- Constructor Over-injection: Too many dependencies suggest a class has too many responsibilities
- Circular Dependencies: When two services depend on each other
- Mismatched Lifetimes: Injecting shorter-lived services into longer-lived ones
Best Practices
- Follow SOLID Principles: Especially Single Responsibility and Dependency Inversion
- Keep Services Small and Focused: Each service should have a clear purpose
- Use Constructor Injection: It's more explicit than property or method injection
- Register Services by Interfaces: Promotes loose coupling
- Be Mindful of Service Lifetimes: Choose appropriate lifetimes based on service characteristics
Summary
Dependency injection is a powerful technique that promotes loose coupling, testability, and maintainability in C# applications. ASP.NET Core provides a built-in DI container that makes implementing this pattern straightforward.
Key takeaways:
- DI provides dependencies from outside rather than creating them internally
- Constructor injection is the most common form of DI
- ASP.NET Core's built-in DI container manages registration, resolution, and lifetimes
- Services can have transient, scoped, or singleton lifetimes
- DI greatly simplifies unit testing by allowing mock implementations
By mastering dependency injection, you'll write more modular, testable, and maintainable C# code.
Additional Resources
Exercises
- Create a simple console application that uses dependency injection without any framework.
- Build an ASP.NET Core web API with at least three layers (controller, service, repository) using DI.
- Implement a service with multiple implementations and inject them as IEnumerable<T>.
- Write unit tests for a service that has dependencies, using mocks for those dependencies.
- Refactor an existing application to use dependency injection, identifying and resolving any tight coupling issues.
💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!