DEV Community

Kamil Bugno
Kamil Bugno

Posted on

Dependency injection in .NET Core

Have you ever wondered what DI has to do with sushi, why DI is important and how to achieve it in .NET Core? If yes, this article is for you!

DI and sushi

I love sushi. Dipping into soy sauce with a bit of Wasabi paste make sushi delicious 🍽
Sushi
But what does this have to do with DI?

Let's assume, that I want to prepare sushi on my own. Doing it in C# (haha) might look something like this:

public class SushiService
{
    public Sushi DoSushi()
    {
        var whiteRice = GetWhiteRice();
        var pot = GetPot();
        var boiledRice = pot.Boil(whiteRice);
        var smokedSalmon = GetSmokedSalmon();
        var seaweedSheets = GetSeaweedSheets();

        //some other steps
        //...

        return sushi;
    }

    private SmokedSalmon GetSmokedSalmon() { ... }

    private SeaweedSheets GetSeaweedSheets() { ... }

    private Pot GetPot() { ... }

    private WhiteRice GetWhiteRice() { ... }
}
Enter fullscreen mode Exit fullscreen mode

Why is it not a good idea to prepare sushi without DI?

It can seem correctly, but there are several drawbacks of this solution. For example, the above code assumes, that we can use only one, specific pot. Despite the fact, that in my cupboard there are a lot of pots and all of them can be used to boil something, I don't have sufficient freedom to choose them. What is more, if I have already boiled rice (because I prepared too much for a previous meal), I cannot use it for my sushi - sushi method requires to prepare new rice. Analogous situation is for ingredients, I can have multiple seaweed sheets or smoked salmons (one in the fridge, another in the freezer, etc.), but I am able to use only one of them no matter how many times and when I will be making sushi. As you can see, it is not the best way of preparing sushi.

Sushi with DI is delicious

Don't worry, you don't have to be hungry - we can simply refactor our solution and with the power of dependency injection prepare delicious sushi.

The first step is to create abstraction that is responsible for certain steps of preparing sushi.

public class SushiService
{
    private IRiceRepository _riceRepository;
    private ISalmonRepository _salmonRepository;
    private ISeaweedSheetsRepository _seaweedSheetsRepository;
    private IPotRepository _potRepository;

    public SushiService(IRiceRepository riceRepository, 
                        ISalmonRepository salmonRepository,
                        ISeaweedSheetsRepository seaweedSheetsRepository, 
                        IPotRepository potRepository)
    {
        _riceRepository = riceRepository;
        _salmonRepository = salmonRepository;
        _seaweedSheetsRepository = seaweedSheetsRepository;
        _potRepository = potRepository;
    }

    public Sushi DoSushi()
    {
        var whiteRice = _riceRepository.GetRice();
        var pot = _potRepository.GetPot();
        var boiledRice = pot.Boil(whiteRice);
        var smokedSalmon = _salmonRepository.GetSmokedSalmon();
        var seaweedSheets = _seaweedSheetsRepository.GetSeaweedSheets();

        //some other steps
        //...

        return sushi;
    }
 }
Enter fullscreen mode Exit fullscreen mode

Advanteges of DI

As a result, our sushi is not dependent on the exact source of salmon or pot instance. It uses repositories to retrieve the desired objects without knowing about the details. From the SushiService perspective it doesn't matter what exact pot we use and how to obtain it - from now on these considerations belong to IPotRepository.

We can also modify the way of getting boiled rice. Since we use only boiled rice, we can create another piece of abstraction, that is responsible for providing SushiService with boiled rice:

 public class SushiService
 {
     private IRiceService _riceService;
     private ISalmonRepository _salmonRepository;
     private ISeaweedSheetsRepository _seaweedSheetsRepository;
     public SushiService(IRiceService riceService, 
                         ISalmonRepository salmonRepository, 
                         ISeaweedSheetsRepository seaweedSheetsRepository)
     {
         _riceService = riceService;
         _salmonRepository = salmonRepository;
         _seaweedSheetsRepository = seaweedSheetsRepository;
     }

     public Sushi DoSushi()
     {
         var boiledRice = _riceService.GetBoiledRice();
         var smokedSalmon = _salmonRepository.GetSmokedSalmon();
         var seaweedSheets = _seaweedSheetsRepository.GetSeaweedSheets();

         //some other steps
         //...

         return sushi;
     }
 }
Enter fullscreen mode Exit fullscreen mode

So, we don't have to worry about the process of boiling our rice and what piece of equipment we need to use for boiling. All of it is done by IRiceService. The advantage of this solution is the fact, that we can reuse our IRiceService in different situations. You don't have to write the same piece of code every time when you want to use boiled rice:

 //...
 var whiteRice = _riceRepository.GetRice();
 var pot = _potRepository.GetPot();
 var boiledRice = pot.Boil(whiteRice);
 //...
Enter fullscreen mode Exit fullscreen mode

Instead, you can simply call GetBoiledRice from IRiceService. Thanks to this, when the method of preparing boiled rice change, you will modify only one place - RiceService.

It is also good to know that next benefit of using DI is the testability aspect. Let's imagine that ISalmonRepository sends HTTP request to order salmon online and it will be shipped to your house. It will be a waste of money to buy salmon each time when you want to check if your method of preparing sushi works. For example, you can ASSUME that you have the salmon and use this assumption when you verify other steps. This is the way how we can do it in C# for all dependencies:

 [Fact]
 public void VerifySushiMethod()
 {
     //Arrange
     var riceServiceMock = new Mock<IRiceService>();
     var salmonRepositoryMock = new Mock<ISalmonRepository>();
     var seaweedSheetsRepositoryMock = new Mock<ISeaweedSheetsRepository>();
     //...
     var sushiService = new SushiService(riceServiceMock.Object,
                salmonRepositoryMock.Object,
                seaweedSheetsRepositoryMock.Object);

     //Act
     var sushi = sushiService.DoSushi();

     //Assert
     //...
  }
Enter fullscreen mode Exit fullscreen mode

DI in .NET Core

The last step that is required to complete the process is to inject the dependencies. In .NET Core there is a built-in container that help you to use DI. In Startup class there is ConfigureServices method that you can use to specify the dependencies:

  public void ConfigureServices(IServiceCollection services)
  {
       //...
       services.AddTransient<IRiceService, RiceService>();
       services.AddTransient<ISeaweedSheetsRepository, SeaweedSheetsRepository>(); 
       services.AddTransient<ISalmonRepository, SalmonRepository>();
       //...
  }
Enter fullscreen mode Exit fullscreen mode

.NET Core provides you with the option to control the lifetime of injected services. There are three types to choose from:

  • AddSingleton - it creates one object that will be reused for all requests.
  • AddTransient - it creates an object each time when it is requested from the code, so the new instance is provided to every class that use the specified interface.
  • AddScoped - it creates the object for each client request, so the object will be different across different client requests.

Summary

To sum up, using DI is extremely useful because it makes your code more readable, testable, and flexible. As you can see, it is not hard to write a piece of code that use the power of dependency injection. I might even risk saying that it's easier to implement it in C# than making real sushi yourself.

Discussion (0)