This is an example code, all of which is written in .NET 6. You can find the specific code in this repository Articles.DI.
In the previous articles, we mentioned the basic usage of dependency injection. We used a simple example, registered the IMessageWriter
interface, and wrote two implementation classes, MessageWriter
and LoggingMessageWriter
, but both of them only have one constructor. If we have multiple constructors in the implementation class when registering the service, how does the container choose?
How to choose constructors#
We can write code directly to simulate this scenario. There is a service called ExampleService, which has multiple constructors. These constructors require different numbers and types of parameters. Please refer to the following code:
// https://github.com/alva-lin/Articles.DI/tree/master/WorkerService3
public class ExampleService
{
public ExampleService() => Console.WriteLine("Empty constructor");
public ExampleService(AService aService) =>
Console.WriteLine("Single parameter constructor: AService");
public ExampleService(AService aService, BService bService) =>
Console.WriteLine("Double parameter constructor: AService, BService");
public ExampleService(AService aService, CService cService) =>
Console.WriteLine("Double parameter constructor: AService, CService");
}
public class AService
{
public AService() => Console.WriteLine("AService instantiated");
}
public class BService
{
public BService() => Console.WriteLine("BService instantiated");
}
public class CService
{
public CService() => Console.WriteLine("CService instantiated");
}
The ExampleService
class has four constructors, each of which depends on three services. When registering the service, we only register AService
and BService
.
IHost host = Host.CreateDefaultBuilder(args)
.ConfigureServices(services =>
{
services.AddHostedService<Worker>();
services.AddSingleton<ExampleService>();
// Try commenting (or uncommenting) the following code to form different combinations and run to see the output result
services.AddSingleton<AService>();
services.AddSingleton<BService>();
// services.AddSingleton<CService>();
})
.Build();
await host.RunAsync();
public class Worker : BackgroundService
{
private readonly ExampleService _exampleService;
// Inject an instance of ExampleService, but which constructor of it is called?
public Worker(ExampleService exampleService)
{
_exampleService = exampleService;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
// Do nothing
}
}
The result of the above code execution is:
AService instantiated
BService instantiated
Double parameter constructor: AService, BService
From the result, we can see that when the container instantiates the ExampleService
class, it uses the third constructor. Comparing all the constructors, the third and fourth constructors require the most parameters, and the fourth constructor requires the CService
class, which we did not register in the container, so the container will not choose the fourth constructor. Therefore, we can understand that the container follows these rules when choosing constructors:
Rule 1: The parameter types required by the constructor must be registered in the container.
Rule 2: Choose the constructor with the most parameters as much as possible.
If we register the CService
together when registering the service and run it again, the program will throw an error:
Unhandled exception. System.AggregateException:
Some services are not able to be constructed
(Error while validating the service descriptor
'ServiceType: Microsoft.Extensions.Hosting.IHostedService
Lifetime: Singleton
ImplementationType: WorkerService3.Worker':
Unable to activate type 'WorkerService3.ExampleService'.
The following constructors are ambiguous:
Void .ctor(WorkerService3.AService, WorkerService3.BService)
Void .ctor(WorkerService3.AService, WorkerService3.CService))
...
The error message indicates that the ExampleService
type cannot be constructed because there is ambiguity between the two constructors and cannot be selected. Therefore, we can know the third rule:
Rule 3: If there are multiple constructors that satisfy the previous rules at the same time, an exception will be thrown.
Dependency Graph#
In the above code, the Worker
class depends on the ExampleService
class, and the ExampleService
class depends on other classes, forming a chain of dependencies. When the container instantiates the Worker
class, it will find its constructor, and the Worker
class only has one constructor, which declares the need for an instance of type ExampleService
, so the container will continue to instantiate the ExampleService
class and find its constructor. The ExampleService
class has multiple constructors, and the container will choose the most suitable one based on the actual situation.
The code flow of this article is as follows:
- When creating the HostBuilder, register the background service
Worker
and other services (services.add...
). - Start the background service, which is the
Worker
class (await host.RunAsync();
). - The container instantiates the
Worker
class, finds its constructor, and resolves the required parameters. TheExampleService
class is found. - The container instantiates the
ExampleService
class and finds that it has multiple constructors. - Starting from the constructor with the most parameters, compare whether its conditions can be met and select the most suitable one.
- Select the third constructor, instantiate
AService
andBService
because their constructors are simple and can be directly generated. - Inject the
AService
andBService
instances into theExampleService
class to complete the instantiation. - Inject the
ExampleService
instance into theWorker
class to complete the instantiation.
From the Worker
class to the ExampleService
class, and then to the AService
and BService
classes, this is a tree-like dependency relationship. When the container instantiates the Worker
class, it will recursively build the required services based on this dependency relationship.
Summary#
The rules for the container to choose constructors when building instances are as follows:
Rule 1: The parameter types required by the constructor must be registered in the container.
Rule 2: Choose the constructor with the most parameters as much as possible.
Rule 3: If there are multiple constructors that satisfy the previous rules at the same time, an exception will be thrown.
In complex programs, the container will analyze the dependencies of services. Starting from the deepest part of the dependency tree, it will build and inject dependencies recursively to construct the final required services.