0

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.

Steven
  • 166,672
  • 24
  • 332
  • 435
Dean P
  • 1,841
  • 23
  • 23
  • 1
    Person class should not be dependent on Calculator. Calculator should have method Calculate which takes person as a parameter and performs calculations and returns. – Chetan Mar 27 '22 at 16:08
  • The purpose of the code is to demonstrate the problem, not to emulate best practice patterns (except of course the DI pattern itself) – Dean P Mar 27 '22 at 16:12
  • 1
    The problem you have mentioned is not only with Console Application. It will occur with any application where Person object is manually initialized. You need to get the dependency of Person when you create an object of it. So the problem is rather incorrect design and not why DI is not able to solve the issue.. – Chetan Mar 27 '22 at 16:19
  • You can try to use `ActivatorUtilities.CreateInstance` to create a `Person` instance. This method will automatically resolve and inject dependencies. – Serg Mar 27 '22 at 16:58
  • 1
    This is a design issue. Person class shows SOC (Separation of Concerns) violation and DI code smell by depending on run time data. (Even for a simplified example). Create a separate service whose responsibility is to take a person and calculate their net worth – Nkosi Mar 27 '22 at 16:59

1 Answers1

5

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);
    }
Nkosi
  • 235,767
  • 35
  • 427
  • 472