Alva

Alva

programmer
github
telegram
email

Dependency Injection in dotnet - 03. Dependency Relationships and Constructor Discovery Rules

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:

  1. When creating the HostBuilder, register the background service Worker and other services (services.add...).
  2. Start the background service, which is the Worker class (await host.RunAsync();).
  3. The container instantiates the Worker class, finds its constructor, and resolves the required parameters. The ExampleService class is found.
  4. The container instantiates the ExampleService class and finds that it has multiple constructors.
  5. Starting from the constructor with the most parameters, compare whether its conditions can be met and select the most suitable one.
  6. Select the third constructor, instantiate AService and BService because their constructors are simple and can be directly generated.
  7. Inject the AService and BService instances into the ExampleService class to complete the instantiation.
  8. Inject the ExampleService instance into the Worker 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.

Dependency injection in .NET

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.