Prologue

.NET supports the dependency injection (DI) software design pattern, which is a technique for achieving Inversion of Control (IoC) between classes and their dependencies. Dependency injection in .NET is a built-in part of the framework, along with configuration, logging, and the options pattern.

Microsoft Learn

In this article, I’ll talk about the basic use of DI in a .NET Core Web API project.


Further Explanation

Well, although the concept is easy to understand, but you may still feel confused when coding. For example, let’s take a look at Startup.cs, in this file, you’ll see many interfaces, and yet you never find a place that these parameters are passed to Startup class. How does it work? 😵‍💫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Startup
{
public Startup(IConfiguration configuration)
{
// ...
}

public void ConfigureServices(IServiceCollection services)
{
// ...
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
// ...
}
}

Well, this is how DI works, but it is more than just DI. Because here, DI is done by .NET framework automatically, so you don’t feel its presence.

Here, DI is done automatically, but it doesn’t mean that DI is always done automatically! For your own classes, you still have to manually resolve specific instances for injection.


Configure DI

Dependency Injection is used to achieve IoC, and Microsoft .NET framework already provided us a default implementation, which is the services of type IServiceCollection. Just like its name, it is just the container which holds all “things” to be resolved for injection. When we want to resolve things we’ve put into it, we’ll need its friend IServiceProvider. We can get it by IServiceCollection.BuildServiceProvider() or simply by another DI.

1
var provider = services.BuildServiceProvider();

So, since this IServiceCollection is a container, we should put things into it before we get it from it. Of course

As you may have get it before, DI simply get the corresponding implementation of the interface you provided. So what you add into it is actually a mapping from an interface to a concreate implementation class. DI will then create such instance for you when corresponding interface encountered.

There are three strategies for instantiation - transient, scoped and singleton. Easy to understand, transient instance will be created every time, scoped will only be created once every scope (might be a request), while singleton, you know, only one across the whole application. If you are concerned about thread safety, you’d better choose transient.

1
2
3
4
// There are many overloads, here is only the common case
IServiceCollection IServiceCollection.AddTransient<IClass, Class>();
IServiceCollection IServiceCollection.AddScoped<IClass, Class>(...);
IServiceCollection IServiceCollection.AddSingleton<IClass>(Instance);

After we registered mapping to the collection, we can then resolve them by its IServiceProvider.

1
2
IClass? IServiceProvider.GetService<IClass>();
IClass IServiceProvider.GetRequiredService<IClass>();

Automatic DI

There are some common classes where DI will be applied automatically - Startup, Controllers and Services. We’ve seen Startup above, so here let’s have a look at Controllers and Services. Some properties and annotations are omitted to simplify the code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddTransient<IRegisterService, RegisterService>();
// ...
}
}

public class RegisterController : Controller
{
private readonly ILogger<TController> _logger;
private readonly IRegisterService _service;

public RegisterController(ILogger<RegisterController> logger, IRegisterService service)
{
_logger = logger;
_service = service;
}
}

public class RegisterService : IRegisterService
{
protected readonly IServiceProvider _provider;
protected readonly IUnitOfWork _unitOfWork;
protected readonly IMapper _mapper;

protected RegisterService(IServiceProvider provider, IUnitOfWork unitOfWork, IMapper mapper)
{
_provider = provider;
_unitOfWork = unitOfWork;
_mapper = mapper;
}
}

Controllers are created by .NET framework, and the framework can automatically detect the parameters and use DI to inject correct instance to it. And the IRegisterService here is what we registered in Startup.cs, which will also be resolved automatically then.

There may be some other default interfaces for injection, but for now, I haven’t met them. So I guess it could be quite sufficient. 😉


Epilogue

Dependency Injection and Inverse of Control are design patterns, not just implementations. So the idea behind them is what matters most. 😌

I think this is already a good start off. Hope that I can excel later.