The example code in this article uses .NET 6. You can find the specific code in this repository Articles.DI.
In the previous articles, we learned about the three lifetimes of services. So what APIs does the framework provide for registering services? How do we write code to declare services and their specific implementations based on our needs? This article will explore the APIs provided by the built-in DI framework in .NET and their usage.
The .NET API browser provides detailed API documentation. This link shows the APIs related to registering services.
Registration Methods#
There are definitely more methods for registering services than just Add{LifeTime}<TService, TImplementation>()
. You can see the specific method types by clicking this link and checking the table provided in the documentation. I have directly pasted the table below.
Method | Auto Release | Multiple Implementations | Pass Parameters |
---|---|---|---|
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>() | Yes | Yes | No |
Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION}) | Yes | Yes | Yes |
Add{LIFETIME}<{IMPLEMENTATION}>() | Yes | No | No |
AddSingleton<{SERVICE}>(new {IMPLEMENTATION}) | No | Yes | Yes |
AddSingleton(new {IMPLEMENTATION}) | No | No | Yes |
The table above only lists the methods that support generics. There are also methods like Add{LIFETIME}(typeof({SERVICE}), typeof({IMPLEMENTATION}))
, which are essentially the same as the generic methods, but I won't list them here. The last three columns of the table describe the limitations of these methods.
Service Release#
The container is responsible for building services, but who is responsible for releasing a service when it expires? Is it the service consumer or the container?
In the Auto Release
column of the table, if it is marked as "Yes," it means that services registered using these methods will be released by the container. If any of these services implement the IDisposable
interface, the container will automatically call the Dispose
method without the need to explicitly call the release method in the code.
Looking at the table, you can see that services registered using AddSingleton<{SERVICE}>(new {IMPLEMENTATION})
and AddSingleton(new {IMPLEMENTATION})
will not be automatically released. The container framework will not release them on its own, so developers need to take responsibility for releasing these services.
Multiple Implementations#
In the previous examples, we registered a single interface and a single implementation class, and when injecting dependencies, we injected a single service. But what if we want to register multiple implementations as the same interface? Which methods should we use?
In the Multiple Implementations
column of the table, you can see that two types of methods do not support multiple implementations: Add{LIFETIME}<{IMPLEMENTATION}>()
and AddSingleton(new {IMPLEMENTATION})
. By looking at the method descriptions, you can understand that the former directly registers the implementation class of the IMPLEMENTATION
type as a service of the IMPLEMENTATION
type, so it cannot achieve multiple implementations. The latter is similar, as it directly registers a single instance, so it cannot achieve multiple implementations either.
Here, "multiple implementations" refers to multiple implementation classes that implement the same interface. When registering services, they are registered to the same interface. In the previous article, we used a single implementation class to implement multiple interfaces, and when registering services, this class was registered for multiple interfaces. These two cases are different, and the latter does not count as "multiple implementations."
Next, let's use code to demonstrate how to achieve "multiple implementations":
// https://github.com/alva-lin/Articles.DI/tree/master/WorkerService4
// 1. Define an interface IMyDependency and multiple implementation classes that implement this interface
public interface IMyDependency
{
string Id { get; }
bool Flag { get; }
}
public class MyDependency0 : IMyDependency
{
public string Id { get; } = Guid.NewGuid().ToString()[^4..];
public bool Flag { get; init; } = false;
}
public class MyDependency1 : MyDependency0 {}
public class MyDependency2 : MyDependency0 {}
public class MyDependency3 : MyDependency0 {}
public class MyDependency4 : MyDependency0 {}
// 2. Register services
using Microsoft.Extensions.DependencyInjection.Extensions;
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
// Register multiple implementations of an interface
services.AddTransient<IMyDependency, MyDependency1>();
services.AddTransient<IMyDependency, MyDependency2>();
// TryAdd will not register MyDependency3 as its implementation for IMyDependency
// because IMyDependency has already been registered
services.TryAddTransient<IMyDependency, MyDependency3>();
// TryAddEnumerable will not register the same implementation if it has already been registered
// IMyDependency -> MyDependency4, this relationship has not been registered, so it can be successfully registered
var descriptor = new ServiceDescriptor(
typeof(IMyDependency),
typeof(MyDependency4),
ServiceLifetime.Transient);
services.TryAddEnumerable(descriptor);
// It will not check whether the interface has been registered or whether this relationship has been registered, and will register it repeatedly
// MyDependency2 has been registered twice
// So when getting IEnumerable<IMyDependency>
// There will be two instances of type MyDependency2
services.AddTransient<IMyDependency, MyDependency2>(_ => new MyDependency2
{
Flag = true // Construct this service using a factory method to differentiate it from the last registered service
});
})
.Build();
Fun(host.Services);
static void Fun(IServiceProvider serviceProvider)
{
using var scopeServices = serviceProvider.CreateScope();
var services = scopeServices.ServiceProvider;
var myDependency = services.GetRequiredService<IMyDependency>();
GetData(myDependency);
Console.WriteLine();
var list = services.GetRequiredService<IEnumerable<IMyDependency>>();
foreach (var dependency in list)
{
GetData(dependency);
}
Console.WriteLine();
}
static void GetData<T>(T item) where T : IMyDependency
{
Console.WriteLine($"{item.GetType().Name} {item.Id}, {item.Flag}");
}
The program's output is as follows:
MyDependency2 c432, True
MyDependency1 ea48, False
MyDependency2 9b9a, False
MyDependency4 c4ce, False
MyDependency2 77e9, True
Looking at the entire program's output, because all services are registered with the Transient
lifetime, each instance has a different Id.
Looking at the first line of output, the item.Flag
is True
, which means that when resolving the IMyDependency
type, the last successfully registered implementation class will be constructed and injected (i.e., the myDependency
variable in this example).
Comparing the next four lines of output, their order is exactly the order of service registration. When obtaining the IEnumerable<IMyDependency>
type variable, all successfully registered types will be injected in the order of registration. The second and fourth lines both show MyDependency2
, indicating that it is possible to register the same implementation multiple times when registering services.
Passing Parameters#
Method | Auto Release | Multiple Implementations | Pass Parameters |
---|---|---|---|
Add{LIFETIME}<{SERVICE}, {IMPLEMENTATION}>() | Yes | Yes | No |
Add{LIFETIME}<{SERVICE}>(sp => new {IMPLEMENTATION}) | Yes | Yes | Yes |
Add{LIFETIME}<{IMPLEMENTATION}>() | Yes | No | No |
AddSingleton<{SERVICE}>(new {IMPLEMENTATION}) | No | Yes | Yes |
AddSingleton(new {IMPLEMENTATION}) | No | No | Yes |
Passing parameters means that when the container constructs a service instance, it can use a factory method to construct the instance in a more diverse way, rather than simply calling the constructor. Looking at this table, if a factory method is passed, the Pass Parameters
column is marked as "Yes," otherwise it is marked as "No." In fact, even if a factory method is not passed, it is still possible to pass parameters through other means.
For example, using the IOperation<>
type in the constructor. Register the required configuration data in IOperation<MyOperation>
so that you can get the required real-time data in the constructor, achieving the purpose of passing parameters. The specific method of obtaining the configuration data can be placed in the registration method of IOperation<MyOperation>
.