Dependency Injection in.NET: Dependency and Constructor Discovery Rules

The sample code for this article uses. NET 6, specific code can be in this repository Articles.DI Get in.

In the previous article, we mentioned the basic use of Dependent Injection. We used a simple case, registered the IMessageWriter interface, and wrote two implementation classes, MessageWriter and LoggingMessageWriter, but both had only one constructor. How do containers choose when we register a service and the implementation class has multiple constructors?

How to select a constructor

We can write code directly to simulate this scenario. There is a service ExampleService, which has several constructors, how many parameters are needed for these constructors, and the types are different. See the code below:

// 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("Two-parameter constructor: AService, BService");

    public ExampleService(AService aService, CService cService) =>
        Console.WriteLine("Two-parameter constructor: AService, CService");
}

public class AService
{
    public AService() => Console.WriteLine("AService instantiation");
}

public class BService
{
    public BService() => Console.WriteLine("BService instantiation");
}

public class CService
{
    public CService() => Console.WriteLine("CService instantiation");
}

The ExampleService class has four constructors that depend on three services. When we register a 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 code below to form different combinations and run to see the output
        services.AddSingleton<AService>();
        services.AddSingleton<BService>();
        // services.AddSingleton<CService>();
    })
   .Build();

await host.RunAsync();

public class Worker : BackgroundService
{
    private readonly ExampleService _exampleService;

    // An instance of ExampleService was injected, but which of its constructors was called?
    public Worker(ExampleService exampleService)
    {
        _exampleService = exampleService;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        // Do nothing
    }
}

The result of executing the above code is

AService instantiation
BService instantiation
 Two-parameter constructor: AService, BService

As you can see from the results, the container uses the third constructor when instantiating the ExampleService class. Compared with all constructors, the third and fourth constructors require the most parameters, while the fourth constructor requires the CService class. We did not register this service in the container, so the container will not select the fourth constructor. Then we can see that containers choose part of the rules for constructors:

Rule 1: The parameter type required for the constructor must be registered in the container;
Rule 2: Select the constructor with the most parameters possible;

If we register the service with CService and run it again, the program will make 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 built, that the two constructors are ambiguous and cannot be selected. So we know the third rule:

Rule 3: If there are multiple constructors that satisfy the previous rule, an exception will be thrown.

Dependency Diagram

In the above code, the Worker class relies on the ExampleService class, and the ExampleService class relies on other classes to form a chain dependency. When the container instantiates the Worker class, it will only have one constructor depending on which one is found. It declares that an example of the ExampleService type is needed, and then the container continues to instantiate the ExampleService class. Find its constructor, and the ExampleService class has multiple constructors, and the container will choose the most appropriate one, depending on the situation.

The code flow for this article is as follows:

  1. When you create the HostBuilder, register the Background Service Worker and other services (services.add...);
  2. Start the background service, the Worker class (await host.RunAsync();)
  3. The container instantiates the Worker class, finds its constructor, and parses the required parameters. ExampleService class found;
  4. The container instantiates the ExampleService class and finds that it has multiple constructors;
  5. Starting with the constructor with the largest number of parameters, compare whether it meets the conditions and select the one that best meets the needs.
  6. Select the third constructor to instantiate AService and BService because they are simple and can be generated directly.
  7. Instances of AService and BService are injected into the ExampleService class to complete the instantiation.
  8. Instances of ExampleService are injected into the Worker class to complete the instantiation;

From the Worker class to the ExampleService class to AService and BService, this is a tree dependency. When the container instantiates the Worker class, based on this dependency, it goes deeper one by one to generate a dependency and inject it recursively.

summary

When a container builds an instance, the rules for selecting a constructor are as follows:

Rule 1: The parameter type required for the constructor must be registered in the container;
Rule 2: Select the constructor with the most parameters possible;
Rule 3: If there are multiple constructors that satisfy the previous rule, an exception will be thrown.

In complex programs, containers analyze the dependencies of services. Start from the deepest part of the dependency tree, build it one by one, reinject it, and build the services you ultimately need in a recursive manner.

Reference Links

Dependency Injection in.NET

Keywords: .NET

Added by crazytoon on Mon, 17 Jan 2022 06:40:20 +0200