Dependency Injection in .NET Core: A Beginner’s Guide

  • Post author:Jik Tailor
  • Reading time:93 mins read
Dependency Injection in .NET Core
Dependency Injection in .NET Core

Dependency Injection in .NET Core: A Beginner’s Guide

Dependency Injection (DI) is one of the most important patterns used in modern software development, especially when working with frameworks like .NET Core. It is not only a design pattern but also a built-in feature in .NET Core, which allows for better code organization, testing, and reusability. In this beginner’s guide, we will explore the concept of Dependency Injection, why it’s important, and how to implement it in .NET Core.

By the end of this article, you’ll have a solid understanding of Dependency Injection in .NET Core, its core concepts, and practical examples to get started.

Introduction to Dependency Injection

Dependency Injection is a design pattern that is used to achieve Inversion of Control (IoC) between classes and their dependencies. It allows you to build loosely coupled applications, which are easier to maintain and test.

In simpler terms, Dependency Injection means that instead of a class instantiating its own dependencies, the dependencies are provided by an external entity (usually referred to as the container). This approach separates concerns and makes the system more modular and testable.

In traditional programming, a class would instantiate its dependencies like this:

C#
public class Car
{
    private Engine _engine;
    
    public Car()
    {
        _engine = new Engine();
    }
    
    public void Start()
    {
        _engine.Run();
    }
}

In this example, Car is tightly coupled to the Engine class. If you need to replace Engine with a DieselEngine, you would need to modify the Car class.

With Dependency Injection, you can decouple these classes like this:

C#
public class Car
{
    private IEngine _engine;

    public Car(IEngine engine)
    {
        _engine = engine;
    }

    public void Start()
    {
        _engine.Run();
    }
}

Now, you can inject any implementation of the IEngine interface into the Car class without modifying it.

Benefits of Dependency Injection

Using Dependency Injection in your .NET Core application provides several key benefits:

1. Loosely Coupled Code

By decoupling classes from their dependencies, you ensure that changes in one class don’t affect others. This makes your codebase more flexible and easier to modify or extend.

2. Improved Testability

Since dependencies are injected, you can easily mock them in unit tests, allowing you to test your classes in isolation. This leads to better unit test coverage and less brittle tests.

3. Simplified Maintenance

When classes are loosely coupled, it’s easier to maintain and refactor the code. You can change implementations without affecting dependent classes.

4. Enhanced Reusability

By designing your application around interfaces and loosely coupled classes, you can reuse classes in different contexts or projects.

5. Centralized Control of Dependencies

The DI container allows you to centralize the management of all your dependencies, making it easier to control and configure them in one place.

Understanding Dependencies in .NET Core

In .NET Core, a dependency is any service or object that a class needs to perform its function. Common examples include logging services, database contexts, or external APIs. Instead of manually creating instances of these dependencies inside your classes, the framework provides them automatically via Dependency Injection.

How Dependency Injection Works in .NET Core

In .NET Core, Dependency Injection is built-in and automatically configured when you create a new project. The framework provides a Service Container that manages all the dependencies, also known as services.

Here’s a basic breakdown of how DI works in .NET Core:

  1. Service Registration: Services (dependencies) are registered in the DI container during the application startup.
  2. Service Resolution: When a class requires a dependency, the DI container provides the appropriate instance.
  3. Service Injection: The dependency is injected into the class, typically via its constructor.

A typical .NET Core application uses the Startup.cs file to configure services and middleware. Here’s a simplified example:

C#
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // Registering a service in the DI container
        services.AddSingleton<IWeatherService, WeatherService>();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        // Configure the HTTP request pipeline
    }
}

In the ConfigureServices method, services are registered with the DI container. When the application starts, .NET Core automatically resolves the dependencies and injects them where needed.

Types of Dependency Lifetimes

When registering a service in the DI container, you can specify the lifetime of the service. There are three primary lifetimes in .NET Core:

1. Transient

  • When to use: For lightweight, stateless services.
  • Scope: A new instance is created every time the service is requested.

Example:

C#
services.AddTransient<IMyService, MyService>();

2. Scoped

  • When to use: For services that should be created once per request.
  • Scope: A single instance is created per request and shared within the request.

Example:

C#
services.AddScoped<IMyService, MyService>();

3. Singleton

  • When to use: For services that should live for the entire duration of the application.
  • Scope: A single instance is created and shared across all requests.

Example:

C#
services.AddSingleton<IMyService, MyService>();

Registering Services in the Dependency Injection Container

In a typical .NET Core application, you’ll register services in the Startup.cs file using the ConfigureServices method. There are various ways to register services:

Registering by Interface and Implementation:

C#
services.AddSingleton<IWeatherService, WeatherService>();

Registering by Implementation Type:

C#
services.AddSingleton<WeatherService>();

Registering Instances:

C#
var weatherService = new WeatherService();
services.AddSingleton<IWeatherService>(weatherService);

Using Dependency Injection in Controllers and Services

Once services are registered, you can inject them into your controllers, services, or any class by using constructor injection.

Example: Injecting a Service into a Controller

C#
public class WeatherController : Controller
{
    private readonly IWeatherService _weatherService;

    public WeatherController(IWeatherService weatherService)
    {
        _weatherService = weatherService;
    }

    public IActionResult GetWeather()
    {
        var weather = _weatherService.GetCurrentWeather();
        return Ok(weather);
    }
}

In this example, the WeatherController depends on IWeatherService. Instead of creating an instance of WeatherService inside the controller, it’s injected via the constructor.

Constructor Injection vs Method Injection vs Property Injection

There are three common ways to inject dependencies in .NET Core:

1. Constructor Injection (Most Common)

Dependencies are injected through the class constructor. This is the most common and recommended way in .NET Core.

2. Method Injection

Dependencies are passed as method parameters.

C#
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILogger<Startup> logger)
{
    logger.LogInformation("Application is starting.");
}

3. Property Injection

Dependencies are injected through properties. This is less common and can lead to complications if not used carefully.

Advanced Dependency Injection Concepts

1. Factory Injection

If you need to inject dependencies conditionally or based on runtime logic, you can use a factory method.

C#
services.AddTransient<IWeatherService>(provider =>
{
    var config = provider.GetService<IConfiguration>();
    if (config.GetValue<bool>("UseMockService"))
    {
        return new MockWeatherService();
    }
    return new WeatherService();
});

2. Named Dependencies

Sometimes, you need to register multiple implementations of the same interface. .NET Core DI doesn’t natively support named dependencies, but you can implement this functionality manually by using factories or by injecting IServiceProvider to resolve specific types.

Testing with Dependency Injection

DI simplifies testing by making it easy to replace real implementations with mocks or stubs in your unit tests.

Example: Unit Testing with Dependency Injection

C#
public class WeatherControllerTests
{
    [Fact]
    public void GetWeather_ReturnsCorrectWeather()
    {
        // Arrange
        var mockWeatherService = new Mock<IWeatherService>();
        mockWeatherService.Setup(service => service.GetCurrentWeather())
            .Returns(new Weather() { Temperature = 72 });

        var controller = new WeatherController(mockWeatherService.Object);

        // Act
        var result = controller.GetWeather();

        // Assert
        Assert.IsType<OkObjectResult>(result);
    }
}

In this example, the IWeatherService is mocked, allowing you to test the WeatherController in isolation without worrying about external dependencies.

Conclusion

Dependency Injection is a powerful design pattern that is fully integrated into .NET Core. It allows for building loosely coupled, testable, and maintainable applications. Whether you are new to .NET Core or looking to refine your understanding, this beginner’s guide provides a solid foundation for using Dependency Injection in your projects.

By mastering Dependency Injection, you’ll be able to create more flexible and scalable applications that are easier to test and maintain. Embrace the power of DI, and you’ll see the benefits across all aspects of your .NET Core development work.

Now that you understand the basics of Dependency Injection in .NET Core, start implementing it in your projects and explore more advanced concepts like factory injection and testing strategies. This foundational knowledge will go a long way in building high-quality software in .NET Core.