Dependency injection
In the preceding example of the controller, there is a _postsService
field that is initialized in the constructor method of the controller by using the new()
constructor:
private readonly PostsService _postsService; public PostsController() { _postsService = new PostsService(); }
That says the PostsController
class depends on the PostsService
class, and the PostsService
class is a dependency of the PostsController
class. If we want to replace PostsService
with a different implementation to save the data, we have to update the code of PostsController
. If the PostsService
class has its own dependencies, they must also be initialized by the PostsController
class. When the project grows larger, the dependencies will become more complex. Also, this kind of implementation is not easy to test and maintain.
Dependency injection (DI) is one of the most well-known design patterns in the software development world. It helps decouple classes that depend on each other. You may find the following terms being used interchangeably: Dependency Inversion Principle (DIP), Inversion of Control (IoC), and DI. These terms are commonly confused even though they are related. You can find multiple articles and blog posts that explain them. Some say they are the same thing, but some say not. What are they?
Understanding DI
The Dependency Inversion Principle is one of the SOLID principles in object-oriented (OO) design. It was defined by Robert C. Martin in his book Agile Software Development: Principles, Patterns, and Practices, Pearson, in 2002. The principle states, “high-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details. Details should depend upon abstractions.”
In the preceding controller, we said PostsController
depends on PostsService
. The controller is the high-level module, and the service is the low-level module. When the service is changed, the controller must be changed as well. Keep in mind that the term inversion does not mean that the low-level module will depend on the high level. Instead, both of them should depend on abstractions that expose the behavior needed by high-level modules. If we invert this dependency relationship by creating an interface for the service, both the controller and the service will depend on the interface. The implementation of the service can change as long as it respects the interface.
IoC is a programming principle that inverts the flow of control in an application. In traditional programming, custom code is responsible for instantiating objects and controlling the execution of the main function. IoC inverts the flow of control as compared to traditional control flow. With IoC, the framework does the instantiation, calling custom or task-specific code.
It can be used to differentiate a framework from a class library. Normally, the framework calls the application code, and the application code calls the library. This kind of IoC is sometimes referred to as the Hollywood principle: “Don’t call us, we’ll call you.”
IoC is related to DIP, but it is not the same. DIP concerns decoupling dependencies between high-level modules and low-level modules through shared abstractions (interfaces). IoC is used to increase the modularity of the program and make it extensible. There are several technologies to implement IoC, such as Service Locator, DI, the template method design pattern, the strategy design pattern, and so on.
DI is a form of IoC. This term was coined by Martin Fowler in 2004. It separates the concerns of constructing objects and using them. When an object or a function (the client) needs a dependency, it does not know how to construct it. Instead, the client only needs to declare the interfaces of the dependency, and the dependency is injected into the client by external code (an injector). It makes it easier to change the implementation of the dependency. It is often similar to the strategy design pattern. The difference is that the strategy pattern can use different strategies to construct the dependency, while DI typically only uses a single instance of the dependency.
There are three main types of DI:
- Constructor injection: The dependencies are provided as parameters of the client’s constructor
- Setter injection: The client exposes a setter method to accept the dependency
- Interface injection: The dependency’s interface provides an injector method that will inject the dependency into any client passed to it
As you can see, these three terms are related, but there are some differences. Simply put, DI is a technique for achieving IoC between classes and their dependencies. ASP.NET Core supports DI as a first-class citizen.
DI in ASP.NET Core
ASP.NET Core uses constructor injection to request dependencies. To use it, we need to do the following:
- Define interfaces and their implementations.
- Register the interfaces and the implementations to the service container.
- Add services as the constructor parameters to inject the dependencies.
You can download the example project named DependencyInjectionDemo
from the folder samples/chapter2/ DependencyInjectionDemo/DependencyInjectionDemo
in the chapter's GitHub repository.
Follow the steps below to use DI in ASP.NET Core:
- First, we will create an interface and its implementation. Copy the
Post.cs
file and thePostService.cs
file from the previousMyFirstApi
project to theDependencyInjectionDemo
project. Create a new interface namedIPostService
in theService
folder, as shown next:public interface IPostService
{
Task CreatePost(Post item);
Task<Post?> UpdatePost(int id, Post item);
Task<Post?> GetPost(int id);
Task<List<Post>> GetAllPosts();
Task DeletePost(int id);
}
Then, update the
PostService
class to implement theIPostService
interface:public class PostsService : IPostService
You may also need to update the namespace of the
Post
class and thePostService
class. - Next, we can register the
IPostService
interface and thePostService
implementation to the service container. Open theProgram.cs
file, and you will find that an instance ofWebApplicationBuilder
named builder is created by calling theWebApplication.CreateBuilder()
method. TheCreateBuilder()
method is the entry point of the application. We can configure the application by using the builder instance, and then call thebuilder.Build()
method to build theWebApplication
. Add the following code:builder.Services.AddScoped<IPostService, PostsService>();
The preceding code utilizes the
AddScoped()
method, which indicates that the service is created once per client request and disposed of upon completion of the request. - Copy the
PostsController.cs
file from the previousMyFirstApi
project to theDependencyInjectionDemo
project. Update the namespace and theusing
statements. Then, update the constructor method of the controller as follows:private readonly IPostService _postsService;
public PostsController(IPostService postService)
{
_postsService = postService;
}
The preceding code uses the
IPostService
interface as the constructor parameter. The service container will inject the correct implementation into the controller.
DI has four roles: services, clients, interfaces, and injectors. In this example, IPostService
is the interface, PostService
is the service, PostsController
is the client, and builder.Services
is the injector, which is a collection of services for the application to compose. It is sometimes referred to as a DI container.
The PostsController
class requests the instance of IPostService
from its constructor. The controller, which is the client, does not know where the service is, nor how it is constructed. The controller only knows the interface. The service has been registered in the service container, which can inject the correct implementation into the controller. We do not need to use the new
keyword to create an instance of the service. That says the client and the service are decoupled.
This DI feature is provided in a NuGet package called Microsoft.Extensions.DependencyInjection
. When an ASP.NET Core project is created, this package is added automatically. If you create a console project, you may need to install it manually by using the following command:
dotnet add package Microsoft.Extensions.DependencyInjection
If we want to replace the IPostService
with another implementation, we can do so by registering the new implementation to the service container. The code of the controller does not need to be changed. That is one of the benefits of DI.
Next, let us discuss the lifetime of services.
DI lifetimes
In the previous example, the service is registered using the AddScoped()
method. In ASP.NET Core, there are three lifetimes when the service is registered:
- Transient: A transient service is created each time it is requested and disposed of at the end of the request.
- Scoped: In web applications, a scope means a request (connection). A scoped service is created once per client request and disposed of at the end of the request.
- Singleton: A singleton service is created the first time it is requested or when providing the implementation instance to the service container. All subsequent requests will use the same instance.
To demonstrate the difference between these lifetimes, we will use a simple demo service:
Create a new interface named IDemoService
and its implementation named DemoService
in the Services
folder, as shown next:
IDemoService.cs:
namespace DependencyInjectionDemo.Services;
public interface IDemoService
{
SayHello();
}
DemoService.cs:
namespace DependencyInjectionDemo.Services;
public class DemoService : IDemoService
{
private readonly Guid _serviceId;
private readonly DateTime _createdAt;public DemoService()
{
_serviceId = Guid.NewGuid();
_createdAt = DateTime.Now;}
public string SayHello()
{
return $"Hello! My Id is {_serviceId}. I was created at {_createdAt:yyyy-MM-dd HH:mm:ss}.
";
}
}
The implementation will generate an ID and a time when it was created, and output it when the SayHello()
method is called.
- Then, we can register the interface and the implementation to the service container. Open the
Program.cs
file and add the code as follows:builder.Services.AddScoped<IDemoService, DemoService>();
- Create a controller named
DemoController.cs
. Now, we can add the service as constructor parameters to inject the dependency:[ApiController]
[Route("[controller]")]
public class DemoController : ControllerBase
{
private readonly IDemoService _demoService;
public DemoController(IDemoService demoService)
{
_demoService = demoService;
}
[HttpGet]
public ActionResult Get()
{
return Content(_demoService.SayHello());
}
}
For this example, if you test the /demo
endpoint, you will see the GUID value and the creation time in the output change every time:
http://localhost:5147/> get demo
HTTP/1.1 200 OK
Content-Length: 91
Content-Type: text/plain; charset=utf-8
Date: Fri, 20 Oct 2023 22:06:46 GMT
Server: Kestrel
Hello! My Id is 6ca84d82-90cb-4dd6-9a34-5ea7573508ac. I was created at 2023-10-21 11:06:46.
http://localhost:5147/> get demo
HTTP/1.1 200 OK
Content-Length: 91
Content-Type: text/plain; charset=utf-8
Date: Fri, 20 Oct 2023 22:07:02 GMT
Server: Kestrel
Hello! My Id is 9bc5cf49-661d-45bb-b9ed-e0b3fe937827. I was created at 2023-10-21 11:07:02.
We can change the lifetime to AddSingleton()
, as follows:
builder.Services.AddSingleton<IDemoService, DemoService>();
The GUID values and the creation time values will be the same for all requests:
http://localhost:5147/> get demo HTTP/1.1 200 OK Content-Length: 91 Content-Type: text/plain; charset=utf-8 Date: Fri, 20 Oct 2023 22:08:57 GMT Server: Kestrel Hello! My Id is a1497ead-bff6-4020-b337-28f1d3af7b05. I was created at 2023-10-21 11:08:02. http://localhost:5147/> get demo HTTP/1.1 200 OK Content-Length: 91 Content-Type: text/plain; charset=utf-8 Date: Fri, 20 Oct 2023 22:09:12 GMT Server: Kestrel Hello! My Id is a1497ead-bff6-4020-b337-28f1d3af7b05. I was created at 2023-10-21 11:08:02.
As the DemoController
class only requests the IDemoService
interface once for each request, we cannot differentiate the behavior between scoped
and transient
services. Let us look at a more complex example.
- You can find the example code in the
DependencyInjectionDemo
project. There are three interfaces along with their implementations:public interface IService
{
string Name { get; }
string SayHello();
}
public interface ITransientService : IService
{
}
public class TransientService : ITransientService
{
private readonly Guid _serviceId;
private readonly DateTime _createdAt;
public TransientService()
{
_serviceId = Guid.NewGuid();
_createdAt = DateTime.Now;
}
public string Name => nameof(TransientService);
public string SayHello()
{
return $"Hello! I am {Name}. My Id is {_serviceId}.
I was created at {_createdAt:yyyy-MM-dd HH:mm:ss}.
";}
}
public interface ISingletonService : IService
{
}
public class SingletonService : ISingletonService
{
private readonly Guid _serviceId;
private readonly DateTime _createdAt;
public SingletonService()
{
_serviceId = Guid.NewGuid();
_createdAt = DateTime.Now;
}
public string Name => nameof(SingletonService);
public string SayHello()
{
return $"Hello! I am {Name}. My Id is {_serviceId}.
I was created at {_createdAt:yyyy-MM-dd HH:mm:ss}.
";}
}
public interface IScopedService : IService
{
}
public class ScopedService : IScopedService
{
private readonly Guid _serviceId;
private readonly DateTime _createdAt;
private readonly ITransientService _transientService;
private readonly ISingletonService _singletonService;
public ScopedService(ITransientService transientService, ISingletonService singletonService)
{
_transientService = transientService;
_singletonService = singletonService;
_serviceId = Guid.NewGuid();
_createdAt = DateTime.Now;
}
public string Name => nameof(ScopedService);
public string SayHello()
{
var scopedServiceMessage = $"Hello! I am {Name}. My Id is {_serviceId}.
I was created at {_createdAt:yyyy-MM-dd HH:mm:ss}.
";var transientServiceMessage = $"{_transientService.SayHello()} I am from {Name}.";
var singletonServiceMessage = $"{_singletonService.SayHello()} I am from {Name}.";
return
$"{scopedServiceMessage}{Environment.NewLine}{transientServiceMessage}{Environment.NewLine}{singletonServiceMessage}";
}
}
- In the
Program.cs
file, we can register them to the service container as follows:builder.Services.AddScoped<IScopedService, ScopedService>();
builder.Services.AddTransient<ITransientService, TransientService>();
builder.Services.AddSingleton<ISingletonService, SingletonService>();
- Then, create a controller named
LifetimeController.cs
. The code is shown next:[ApiController]
[Route("[controller]")]
public class LifetimeController : ControllerBase
{
private readonly IScopedService _scopedService;
private readonly ITransientService _transientService;
private readonly ISingletonService _singletonService;
public LifetimeController(IScopedService scopedService, ITransientService transientService,
ISingletonService singletonService)
{
_scopedService = scopedService;
_transientService = transientService;
_singletonService = singletonService;
}
[HttpGet]
public ActionResult Get()
{
var scopedServiceMessage = _scopedService.SayHello();
var transientServiceMessage = _transientService.SayHello();
var singletonServiceMessage = _singletonService.SayHello();
return Content(
$"{scopedServiceMessage}{Environment.NewLine}{transientServiceMessage}{Environment.NewLine}{singletonServiceMessage}");
}
}
In this example, ScopedService
has two dependencies: ITransientService
and ISingletonService
. So, when ScopedService
is created, it will ask for the instances of these dependencies from the service container. On the other hand, the controller also has dependencies: IScopedService
, ITransientService
, and ISingletonService
. When the controller is created, it will ask for these three dependencies. That means ITransientService
and ISingletonService
will be needed twice for each request. But let us check the output of the following requests:
http://localhost:5147/> get lifetime
HTTP/1.1 200 OK
Content-Length: 625
Content-Type: text/plain; charset=utf-8
Date: Fri, 20 Oct 2023 22:20:44 GMT
Server: Kestrel
Hello! I am ScopedService. My Id is df87d966-0e86-4f08-874f-ba6ce71de560. I was created at 2023-10-21 11:20:44.
Hello! I am TransientService. My Id is 77e29268-ad48-423c-94e5-de1d09bd3ba5. I was created at 2023-10-21 11:20:44. I am from ScopedService.
Hello! I am SingletonService. My Id is 95a44c5b-8678-48c6-a2f0-cc6b90423773. I was created at 2023-10-21 11:20:44. I am from ScopedService.
Hello! I am TransientService. My Id is e77564d1-e146-4d29-b74b-a07f8f6640c1. I was created at 2023-10-21 11:20:44.
Hello! I am SingletonService. My Id is 95a44c5b-8678-48c6-a2f0-cc6b90423773. I was created at 2023-10-21 11:20:44.
http://localhost:5147/> get lifetime
HTTP/1.1 200 OK
Content-Length: 625
Content-Type: text/plain; charset=utf-8
Date: Fri, 20 Oct 2023 22:20:57 GMT
Server: Kestrel
Hello! I am ScopedService. My Id is e5f802ed-5e4c-4abd-9213-8f13f97c1008. I was created at 2023-10-21 11:20:57.
Hello! I am TransientService. My Id is daccb91b-438f-4561-9c86-13b02ad8e358. I was created at 2023-10-21 11:20:57. I am from ScopedService.
Hello! I am SingletonService. My Id is 95a44c5b-8678-48c6-a2f0-cc6b90423773. I was created at 2023-10-21 11:20:44. I am from ScopedService.
Hello! I am TransientService. My Id is 94e9e6c1-729a-4033-8a27-550ea10ba5d0. I was created at 2023-10-21 11:20:57.
Hello! I am SingletonService. My Id is 95a44c5b-8678-48c6-a2f0-cc6b90423773. I was created at 2023-10-21 11:20:44.
We can see that in each request, ScopedService
was created once, while ITransientService
was created twice. In both requests, SingletonService
was created only once.
Group registration
As the project grows, we may have more and more services. If we register all services in Program.cs
, this file will be very large. For this case, we can use group registration to register multiple services at once. For example, we can create a service group named LifetimeServicesCollectionExtensions.cs
:
public static class LifetimeServicesCollectionExtensions { public static IServiceCollection AddLifetimeServices(this IServiceCollection services) { services.AddScoped<IScopedService, ScopedService>(); services.AddTransient<ITransientService, TransientService>(); services.AddSingleton<ISingletonService, SingletonService>(); return services; } }
This is an extension method for the IServiceCollection
interface. It is used to register all services at once in the Program.cs
file:
// Group registration builder.Services.AddLifetimeServices();
In this way, the Program.cs
file will be smaller and easier to read.
Action injection
Sometimes, one controller may need many services but may not need all of them for all actions. If we inject all the dependencies from the constructor, the constructor method will be large. For this case, we can use action injection to inject dependencies only when needed. See the following example:
[HttpGet] public ActionResult Get([FromServices] ITransientService transientService) { ... }
The [FromServices]
attribute enables the service container to inject dependencies when needed without using constructor injection. However, if you find that a service needs a lot of dependencies, it may indicate that the class has too many responsibilities. Based on the Single Responsibility Principle (SRP), consider refactoring the class to split the responsibilities into smaller classes.
Keep in mind that this kind of action injection only works for actions in the controller. It does not support normal classes. Additionally, since ASP.NET Core 7.0, the [FromServices]
attribute can be omitted as the framework will automatically attempt to resolve any complex type parameters registered in the DI container.
Keyed services
ASP.NET Core 8.0 introduces a new feature known as keyed services, or named services. This feature allows developers to register services with a key, allowing them to access the service with that key. This makes it easier to manage multiple services that implement the same interface within an application, as the key can be used to identify and access the service.
For example, we have a service interface named IDataService
:
public interface IDataService
{
string GetData();
}
This IDataService
interface has two implementations: SqlDatabaseService
and CosmosDatabaseService
:
public class SqlDatabaseService : IDataService
{
public string GetData()
{
return "Data from SQL Database";
}
}
public class CosmosDatabaseService : IDataService
{
public string GetData()
{
return "Data from Cosmos Database";
}
}
We can register them to the service container using different keys:
builder.Services.AddKeyedScoped<IDataService, SqlDatabaseService>("sqlDatabaseService");
builder.Services.AddKeyedScoped<IDataService, CosmosDatabaseService>("cosmosDatabaseService");
Then, we can inject the service by using the FromKeyedServices
attribute:
[ApiController]
[Route("[controller]")]
public class KeyedServicesController : ControllerBase
{
[HttpGet("sql")]
public ActionResult GetSqlData([FromKeyedServices("sqlDatabaseService")] IDataService dataService) =>
Content(dataService.GetData());
[HttpGet("cosmos")]
public ActionResult GetCosmosData([FromKeyedServices("cosmosDatabaseService")] IDataService dataService) =>
Content(dataService.GetData());
}
The FromKeyedServices
attribute is used to inject the service by using the specified key. Test the API with HttpRepl, and you will see the output as follows:
http://localhost:5147/> get keyedServices/sql
HTTP/1.1 200 OK
Content-Length: 22
Content-Type: text/plain; charset=utf-8
Date: Fri, 20 Oct 2023 22:48:49 GMT
Server: Kestrel
Data from SQL Database
http://localhost:5147/> get keyedServices/cosmos
HTTP/1.1 200 OK
Content-Length: 25
Content-Type: text/plain; charset=utf-8
Date: Fri, 20 Oct 2023 22:48:54 GMT
Server: Kestrel
Data from Cosmos Database
The keyed services can be used to register singleton or transient services as well. Just use the AddKeyedSingleton()
or AddKeyedTransient()
method respectively; for example:
builder.Services.AddKeyedSingleton<IDataService, SqlDatabaseService>("sqlDatabaseService");
builder.Services.AddKeyedTransient<IDataService, CosmosDatabaseService>("cosmosDatabaseService");
It is important to note that if an empty string is passed as the key, a default implementation for the service must be registered with a key of an empty string, otherwise the service container will throw an exception.
Microsoft releases new versions of .NET SDKs frequently. If you encounter a different version number, that is acceptable.
The preceding command will list all the available SDKs on your machine. For example, it may show the following output if have multiple .NET SDKs installed.
Important note
Every Microsoft product has a lifecycle. .NET and .NET Core provides Long-term support (LTS) releases that get 3 years of patches and free support. When this book was written, .NET 7 is still supported, until May 2024. Based on Microsoft’s policy, even numbered releases are LTS releases. So .NET 8 is the latest LTS release. The code samples in this book are written with .NET 8.0.
When you use VS Code to open the project, the C# Dev Kit extension can create a solution file for you. This feature makes VS Code more friendly to C# developers. You can see the following structure in the Explorer view:
It uses the Swashbuckle.AspNetCore
NuGet package, which provides the Swagger UI to document and test the APIs.
Follow the steps below to use DI in ASP.NET Core:
We can see that in each request, ScopedService
was created once, while ITransientService
was created twice. In both requests, SingletonService
was created only once.
Using primary constructors to inject dependencies
Beginning with .NET 8 and C# 12, we can use the primary constructor to inject dependencies. A primary constructor allows us to declare the constructor parameters directly in the class declaration, instead of using a separate constructor method. For example, we can update the PostsController
class as follows:
```csharp public class PostsController(IPostService postService) : ControllerBase { // No need to define a private field to store the service // No need to define a constructor method } ```
You can find a sample named PrimaryConstructorController.cs
in the Controller
folder of the DependencyInjectionDemo
project.
When using the primary constructor in a class, note that the parameters passed to the class declaration cannot be used as properties or members. For example, if a class declares a parameter named postService
in the class declaration, it cannot be accessed as a class member using this.postService
or from external code. To learn more about the primary constructor, please refer to the documentation at https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/instance-constructors#primary-constructors.
Primary constructors can save us from writing fields and constructor methods. So, we’ll use them in the following examples.
Do not use new
to create service B, otherwise, service A will be tightly coupled with service B.
Resolving a service when the app starts
If we need a service in the Program.cs
file, we cannot use constructor injection. For this situation, we can resolve a scoped service for a limited duration at app startup, as follows:
var app = builder.Build(); using (var serviceScope = app.Services.CreateScope()) { var services = serviceScope.ServiceProvider; var demoService = services.GetRequiredService<IDemoService>(); var message = demoService.SayHello(); Console.WriteLine(message); }
The preceding code creates a scope and resolves the IDemoService
service from the service container. Then, it can use the service to do something. After the scope is disposed of, the service will be disposed of as well.
DI tips
ASP.NET Core uses DI heavily. The following are some tips to help you use DI:
- When designing your services, make the services as stateless as possible. Do not use static classes and members unless you have to do so. If you need to use a global state, consider using a singleton service instead.
- Carefully design dependency relationships between services. Do not create a cyclic dependency.
- Do not use
new
to create a service instance in another service. For example, if service A depends on service B, the instance of service B should be injected into service A with DI. Do not usenew
to create service B, otherwise, service A will be tightly coupled with service B. - Use a DI container to manage the lifetime of services. If a service implements the
IDisposable
interface, the DI container will dispose of the service when the scope is disposed of. Do not manually dispose of it. - When registering a service, do not use
new
to create an instance of the service. For example,services.AddSingleton(new ExampleService());
registers a service instance that is not managed by the service container. So, the DI framework will not be able to dispose of the service automatically. - Avoid using the service locator pattern. If DI can be used, do not use the
GetService()
method to obtain a service instance.
You can learn more about the DI guidelines at https://docs.microsoft.com/zh-cn/dotnet/core/extensions/dependency-injection-guidelines.
Why there is no configuration method for the logger in the template project?
ASP.NET Core provides a built-in DI implementation for the logger. When the project was created, logging was registered by the ASP.NET Core framework. Therefore, there is no configuration method for the logger in the template project. Actually, there are more than 250 services that are automatically registered by the ASP.NET Core framework.
Can I use third-party DI containers?
It is highly recommended that you use the built-in DI implementation in ASP.NET Core. But if you need any specific features that it does not support, such as property injection, Func<T>
support for lazy initialization, and so on, you can use third-party DI containers, such as Autofac (https://autofac.org/).