Dependency Injection in .NET6

In short, dependency injection is a methodology by which a code object receives other objects that it dependes upon (called dependencies), rather than those dependencies needing to be instantiated by that object.

Dependency Injection in .NET6
Photo by Dan-Cristian Pădureț / Unsplash

We can compare it to a needle and a thread: the needle must have the thread injected into it before it can do any meaningful work.

Dependency Inversion Principle

The Dependency Inversion Principle states that high level modules should not depend on low level modules; both should depend on abstractions and abstractions should not depend on details. Details should depend upon abstractions.

It's extremely common when writing software to implement it such that each module or method specifically refers to its collaborators, which does the same.
This type of programming typically lacks sufficient layers of abstractions and results in a very tightly coupled system, since every module is directly referencing lower level modules.

Dependency Injection Basics

In .NET6 dependency injection is a "first class citizen" - meaning it is natively supported by the framework. You don't need any third party package.

Dependencies need an abstraction and the dependant class will invoke that abstraction when it needs the corresponding functionality. The concrete implementation can exist elsewhere and the class using the abstraction does not need to know what the concrete implementation is. This way, dependency injection in .NET6 satisfies Dependency Inversion Principle.

FAQ

So, what can be a dependency?
Short anwer: anything with an abstraction.

Where do these dependencies exist?
We register them to the DI container.

How do these dependencies get injected?
.NET will handle this automatically for the registered services.

What kind of issues can we expect?
The main issue is "circular dependencies", but there are others.

What can be a dependency?

Generally speaking, dependencies are small, focused and easily modifiable pieces of code (well, this is a general software design rule).

For example consider the following class that provides access to an in-memory cache (ignoring the built-in InMemoryCache object). We could create such a class and make it injectable using an interface:

public interface ICacheService
{
    void Add(string key, object value);
    T Get<T>(string key);
}

public class CacheService : ICacheService
{
    public void Add(string key, object value)
    {
    	// implementation
    }
    
    public T Get<T>(string key)
    {
    	// implementation
    }
}

Where do these dependencies exist?

A better question here would be "how do we register them so that they get injected automatically?"

Well, out of the box, registering dependencies with the container takes place in the Program.cs file of a .NET6 application.
Here is the default Program.cs file that dotnet new webapi creates for a ASP.NET Core Web API:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

This file does many things but for DI we're only concerned with the builder object, whose type is WebApplicationBuilder. Using this object we can define dependencies, routes and many other things for our web app to work as expected.

How do dependencies get injected?

As mentioned before they are injected automatically by the framework. We just need to register them to the container.

As you can see in the Program.cs file above, there are already a few services registered with the container (Controllers, ApiExplorer and SwaggerGen).

In order to register our CachingService with the container we just need to add this line before calling the Build method on the builder object:

builder.Services.AddTransient<ICacheService, CacheService>();

which makes a "link" between our abstraction ICacheService and our concrete implementation CacheService

Just so there is no confusion, this is how our Program.cs will look like:

...
builder.Services.AddSwaggerGen();

builder.Services.AddTransient<ICacheService, CacheService>();

var app = builder.Build();
...

Now, if you check the boilerplate webapi controller WeatherForecastController.cs you'll notice that the constructor already has a service injected, the logger service.

This is how the controller looks like:


namespace learning.Controllers;

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot",
        "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> _logger;

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet(Name = "GetWeatherForecast")]
    public IEnumerable<WeatherForecast> Get()
    {
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .ToArray();
    }
}

So how do we inject our CacheService?

...
    private readonly ILogger<WeatherForecastController> _logger;
    private readonly ICacheService _cacheService;

    public WeatherForecastController(ILogger<WeatherForecastController> logger, ICacheService cacheService)
    {
        _logger = logger;
        _cacheService = cacheService;
    }
...

As you can see, the controller knows nothing about the concrete class of the caching service... What this means is that we can have multiple concrete implementations of our caching service and inject the one that we need.

If we didn't have dependency injection we would have to refactor all our code in order to replace all the instances of our class.

I will not explain what happens behind the scene in this article just to keep it short but in a future article I'll cover Service Scope and Lifetime which does tackle our "behind the scenes" as well.

Let's just say that in our case, since we used AddTransient, the controller will receive a new instance of our CacheService.

It is as if in the constructor we would have written
_cacheService = new CacheService();
except we don't have a direct dependency on the cache service but instead we only care about its abstraction.

Mastodon Romania