You can also read this post on my blog: blog.kamilbugno.com
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 🍽
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() { ... }
}
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;
}
}
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;
}
}
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);
//...
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
//...
}
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>();
//...
}
.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.
Top comments (1)
Salah, commonly known as prayer, stands as a central pillar of Islam, serving as a direct means of communication between the believer and Allah. It is incumbent upon every adult Muslim to perform Salah five times a day, at prescribed times, as ordained by the Quran and the teachings of Prophet Muhammad (peace be upon him). Salah in Islam is a deeply spiritual act, involving physical and mental purification through ritual ablution and focused devotion.