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
CustomerService
with a mock database - Change the database implementation
- Reuse
CustomerService
with 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
WeatherForecastController
instance - Identifies that it needs an
IWeatherService
implementation - Creates a
WeatherService
(following the scoped lifetime) - Injects it into the controller's constructor
- Calls the
Get
method, 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.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)