How to configure dependency injection "many levels down" using .NET built-in DI Container

933 views Asked by At

I have a console .NET core application which uses the Microsoft.Extensions.DependencyInjection library as a dependency injection framework.

I want to use this framework to inject a dependency two "levels down" without having to redundantly mention this dependency in the middle layer. How do I do this?

Currently, the only way I can inject a dependency is to pass it down the levels until it is needed. Here is my standalone console application that demonstrates the requirement. Its a simple program that calculates a persons net worth based on example asset and liability amounts. (It literally just subtracts the two amounts).

The Program.cs file contains the program Main entry point and registers the dependencies.

Program.cs:

public class Program
{
    private static IServiceProvider _serviceProvider;
    public static void Main(string[] args)
    {
        RegisterServices();
        IServiceScope scope = _serviceProvider.CreateScope();
        scope.ServiceProvider.GetRequiredService<ConsoleApplication>().Run();
        DisposeServices();
    }

    private static void RegisterServices()
    {
        var services = new ServiceCollection();
        services.AddSingleton<ICalculator, Calculator>();
        services.AddSingleton<ConsoleApplication>();
        _serviceProvider = services.BuildServiceProvider(true);
    }

    private static void DisposeServices()
    {
        if (_serviceProvider == null)
        {
            return;
        }
        if (_serviceProvider is IDisposable)
        {
            ((IDisposable)_serviceProvider).Dispose();
        }
    }
}

After setting up the dependency injections, Program.cs runs the ConsoleApplication.cs Run method.

ConsoleApplication.cs:

internal class ConsoleApplication
{
    private readonly ICalculator _calculator;
    public ConsoleApplication(ICalculator calculator)
    {
        _calculator = calculator;
    }

    public void Run()
    {
        Person person = new Person(_calculator)
        {
            Assets = 1000m,
            Liabilities = 300m
        };
        Console.WriteLine(person.GetNetWorth());
    }
}

The above code instantiates an example Person and invokes the GetNetWorth method. The Person class is shown below.

Person.cs:

public class Person
{
    private readonly ICalculator _calculator;

    public Person(ICalculator calculator)
    {
        _calculator = calculator;
    }

    public decimal Assets {get; set;}
    public decimal Liabilities {get; set;}

    public decimal GetNetWorth()
    {
        decimal netWorth = _calculator.Subtract(Assets, Liabilities);
        return netWorth;
    }
}

The Person class has a dependency on Calculator as shown below:

Calculator.cs:

public interface ICalculator
{
    decimal Add(decimal num1, decimal num2);
    decimal Subtract(decimal num1, decimal num2);
    decimal Multiply(decimal num1, decimal num2);
    decimal Divide(decimal num1, decimal num2);
}

public class Calculator : ICalculator
{
    public decimal Add(decimal num1, decimal num2) => num1 + num2;
    public decimal Subtract(decimal num1, decimal num2) => num1 - num2;
    public decimal Multiply(decimal num1, decimal num2) => num1 * num2;
    public decimal Divide(decimal num1, decimal num2) => num1 / num2;
}

Given the program above, you can see the problem here is that the class ConsoleApplication.cs has this line of code:

private readonly ICalculator _calculator;
public ConsoleApplication(ICalculator calculator)
{
    _calculator = calculator;
}

That code is redundant and should be avoided because ConsoleApplication.cs doesn't have this dependency and therefore shouldn't know anything about it. The only reason I'm forced to include it is to pass it the next level down to Person which does need the dependency.

With the example above, how to I adjust the program.cs to avoid passing down the dependency? I have a feeling that I'm using the Microsoft.Extensions.DependencyInjection framework completely wrong. The whole point of using DI containers is to circumvent this problem. I might aswell have not used a DI Container at all and the code would have been simpler.

I have read numerous SO posts asking similar questions. However, they don't cater for my specific case of a console application, .NET and using the Microsoft.Extensions.DependencyInjection; DI Framework.

1

There are 1 answers

0
Nkosi On BEST ANSWER

This is a design issue. Person class shows SRP (Single Responsibility Principle) / SOC (Separation of Concerns) violations and a DI code smell by depending on run time data. (based on the simplified example provided).

Injecting runtime data into your application components is a code smell. Runtime data should flow through the method calls of already-constructed object graphs.

Reference Dependency Injection Code Smell: Injecting runtime data into components

Note how this class is to be constructed with run-time data.

public class Person {
    private readonly ICalculator _calculator;

    public Person(ICalculator calculator) {
        _calculator = calculator;
    }

    public decimal Assets {get; set;} //<--Run-time data
    public decimal Liabilities {get; set;} //<--Run-time data

    public decimal GetNetWorth() {
        decimal netWorth = _calculator.Subtract(Assets, Liabilities);
        return netWorth;
    }
}

Create a separate service whose responsibility is to take a person at run-time and calculate their net worth

//Run-time data
public class Person {        
    public decimal Assets {get; set;}
    public decimal Liabilities {get; set;}
}

//Service abstraction
public interface IPersonNetWorthService {
    decimal GetNetWorth(Person person);
}

//Service implementation
public class PersonNetWorthService : IPersonNetWorthService {
    private readonly ICalculator _calculator;

    public PersonNetWorthService(ICalculator calculator) {
        _calculator = calculator;
    }

    public decimal GetNetWorth(Person person) {
        decimal netWorth = _calculator.Subtract(person.Assets, person.Liabilities);
        return netWorth;
    }
}

Console application is now clean to perform its functionality without any violations and code smells

internal class ConsoleApplication
{
    private readonly IPersonNetWorthService calculator;
    public ConsoleApplication(IPersonNetWorthService calculator) {
        this.calculator = calculator;
    }

    public void Run() {
        Person person = new Person() {
            Assets = 1000m,
            Liabilities = 300m
        };
        Console.WriteLine(calculator.GetNetWorth(person));
    }
}

Remembering to register all dependencies

    private static void RegisterServices() {
        IServiceCollection services = new ServiceCollection();
        services.AddSingleton<IPersonNetWorthService, PersonNetWorthService>();
        services.AddSingleton<ICalculator, Calculator>();
        services.AddSingleton<ConsoleApplication>();
        _serviceProvider = services.BuildServiceProvider(true);
    }