Skip to main content

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:

csharp
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:

csharp
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?

  1. Loose coupling: Classes are no longer responsible for creating their dependencies
  2. Testability: You can easily provide mock implementations during tests
  3. Flexibility: Dependencies can be swapped without changing the dependent classes
  4. Separation of concerns: Classes focus on their core responsibilities
  5. 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:

  1. Registration of services
  2. Resolution of dependencies
  3. 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:

csharp
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:

csharp
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:

  1. Transient: Created each time they're requested
  2. Scoped: Created once per client request
  3. Singleton: Created the first time they're requested and reused for the application's lifetime
csharp
// 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:

csharp
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:

csharp
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+):

csharp
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:

csharp
[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:

  1. Creates a WeatherForecastController instance
  2. Identifies that it needs an IWeatherService implementation
  3. Creates a WeatherService (following the scoped lifetime)
  4. Injects it into the controller's constructor
  5. 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>:

csharp
// 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:

csharp
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:

csharp
// 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:

csharp
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

  1. Service Locator Anti-Pattern: Don't use the DI container directly in your code to locate services
  2. Constructor Over-injection: Too many dependencies suggest a class has too many responsibilities
  3. Circular Dependencies: When two services depend on each other
  4. Mismatched Lifetimes: Injecting shorter-lived services into longer-lived ones

Best Practices

  1. Follow SOLID Principles: Especially Single Responsibility and Dependency Inversion
  2. Keep Services Small and Focused: Each service should have a clear purpose
  3. Use Constructor Injection: It's more explicit than property or method injection
  4. Register Services by Interfaces: Promotes loose coupling
  5. 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

  1. Microsoft Docs: Dependency Injection in ASP.NET Core
  2. .NET Design Patterns
  3. SOLID Principles in C#

Exercises

  1. Create a simple console application that uses dependency injection without any framework.
  2. Build an ASP.NET Core web API with at least three layers (controller, service, repository) using DI.
  3. Implement a service with multiple implementations and inject them as IEnumerable<T>.
  4. Write unit tests for a service that has dependencies, using mocks for those dependencies.
  5. 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! :)