.NET Core Dependency Injection for multilevel, multiple implementation dependencies

2.9k Views Asked by At

I have come across several posts where a single interface is implemented by multiple classes and the dependency is registered and resolved. One such post is this one.

But how to resolve multiple, multi-level dependencies?

For example:

public enum ShortEnum { Short, Long }

public interface ISomeValidator
{
    bool ValidateInputString(string str);
}

public class ConValidator : ISomeValidator
{
    public bool ValidateInputString(string str) => true;
}

public class DonValidator : ISomeValidator
{
    public bool ValidateInputString(string str) => false;
}

public class ConProvider : ISomeProvider 
{
    ISomeValidator conValidator; // Expects instance of ConValidator
    public ConProvider(ISomeValidator someValidator)
    {
        conValidator = someValidator;
    }
}

public class DonProvider : ISomeProvider 
{
    ISomeValidator donValidator; // Expects instance of DonValidator
    public DonProvider(ISomeValidator someValidator)
    {
        donValidator = someValidator;
    }
}

ShortEnum can be used as key. That means depending upon its value, either ConProvider or DonProvider is returned. Now, the providers have a dependency on ISomeValidator, which, again depending upon the key value of ShortEnum can be resolved as the instance of ConValidator or DonValidator.

In other words, I want to build the following two object graphs:

var provider1 = new ConProvider(new ConValidator());
var provider2 = new DonProvider(new DonValidator());

What is the best way to utilize .NET Core 3.1 in-built dependency injection mechanism?

2

There are 2 best solutions below

0
On

If I understood it correctly, you want ConValidator instance for ConProvider and DonValidator instance for DonProvider and Providers should be resolved on ShortEnum value.

we can modify dependencies like below

public interface ISomeProvider
{
    void Method1();
}

public interface ISomeValidator
{
    bool ValidateInputString(string str);
}

public class ConValidator : ISomeValidator
{
    public bool ValidateInputString(string str) => true;
}

public class DonValidator : ISomeValidator
{
    public bool ValidateInputString(string str) => false;
}

public class ConProvider : ISomeProvider
{
    ISomeValidator conValidator; // Expects instance of ConValidator
    public ConProvider(ValidatorResolver validatorResolver) =>
        conValidator = validatorResolver(ShortEnum.Short);

    public void Method1() => Console.WriteLine("Method1 COnProvider");
}

public class DonProvider : ISomeProvider
{
    ISomeValidator donValidator; // Expects instance of DonValidator
    public DonProvider(ValidatorResolver validatorResolver) =>
        donValidator = validatorResolver(ShortEnum.Long);

    public void Method1() => Console.WriteLine("Method1 DonProvider");
}

public enum ShortEnum { Short, Long }

declare delgates

public delegate ISomeProvider ProviderResolver(ShortEnum shortEnum);
public delegate ISomeValidator ValidatorResolver(ShortEnum shortEnum);

Consumer Service

public class SomeOtherService
{
    private readonly ISomeProvider _provider;
    public SomeOtherService(ProviderResolver providerResolver)
    {
        _provider = providerResolver(ShortEnum.Short);
        //OR
        _provider = providerResolver(ShortEnum.Long);
        _provider.Method1();
    }
}

In ConfigureServices method of StartUp class

services.AddTransient<ConValidator>();
services.AddTransient<DonValidator>();
services.AddTransient<ConProvider>();
services.AddTransient<DonProvider>();

services.AddTransient<ProviderResolver>(serviceProvider => (shortEnum) =>
{
    switch (shortEnum)
    {
        case ShortEnum.Short:
            return serviceProvider.GetService<ConProvider>();
        case ShortEnum.Long:
            return serviceProvider.GetService<DonProvider>();
        default:
            throw new KeyNotFoundException();
    }
});

services.AddTransient<ValidatorResolver>(serviceProvider => (shortEnum) =>
{
    switch(shortEnum)
    {
        case ShortEnum.Short:
            return  serviceProvider.GetService<ConValidator>();
        case ShortEnum.Long:
            return serviceProvider.GetService<DonValidator>();
        default:
            throw new KeyNotFoundException();
    }
});

services.AddTransient<SomeOtherService>();
0
On

There are three ways to achieve what you want.

The first option is to fall back to manually wiring the object graphs by registering a delegate that composes the required object graphs completely, for instance:

services.AddTransient<ISomeProvider>(
    c => new ConProvider(new ConValidator());
services.AddTransient<ISomeProvider>(
    c => new DonProvider(new DonValidator());

This, however, will become quite cumbersome once those classes get other dependencies, because you would very quickly start to circumvent the DI Container completely.

So instead, as a second option, you can configure the container in such way that you only have to new the two providers:

// Note how these validators are -not- registered by their interface!
services.AddTransient<ConValidator>(); // Might have dependencies
services.AddTransient<DonValidator>(); // of its own

services.AddTransient<ISomeProvider>(c =>
    new ConProvider(
        someValidator: c.GetRequiredService<ConValidator>(),
        otherDependency1: c:GetRequiredService<IDependency1>());

services.AddTransient<ISomeProvider>(c =>
    new DonProvider(
        someValidator: c.GetRequiredService<DonValidator>(),
        otherDependency2: c:GetRequiredService<IDependency2>());    

This is a more-flexible solution compared to the first. But still, changes to the constructors of ConProvider and DonProvider force you to update their registrations.

To combat this, as a third option, you can use the ActivatorUtilities class. It will help you in achieving Auto-Wiring, where the container figures out which dependencies are required. That might look like this:

services.AddTransient<ConValidator>();
services.AddTransient<DonValidator>();

services.AddTransient<ISomeProvider>(c =>
    ActivatorUtilities.CreateInstance<ConProvider>(
        c,
        c.GetRequiredService<ConValidator>());

services.AddTransient<ISomeProvider>(c =>
    ActivatorUtilities.CreateInstance<DonProvider>(
        c,
        c.GetRequiredService<DonValidator>());

ActivatorUtilities.CreateInstance will create the requested class for you; in this case either ConProvider or DonProvider. It does so by looking at the constructor of the type and resolving all constructor parameters from the supplied c service provider. It resolves all dependencies from the service provider -except- the dependencies you supplied manually to the CreateInstance method. In the example above it will match the ISomeValidator dependency to the supplied DonValidator or ConValidator and inject them instead.