Dependency injection in .Net by Mark Siman 2 - Constructor injection, lifetime
continue the struggle for weak connectivity. In the previous note, we examined the dependencies between the layers of the application, let's move on to smaller forms.
Aggregation, constructor implementation
Objects / classes of the system, like layers, interact with each other. There are dependencies between classes too.
For example, in Listing 1, MyService uses MyDataContext (EF) - it has a MyDataContext dependency.
class MyService
{
public void DoSomething()
{
using(var dbCtx = new MyDataContext())
{
// используем dbCtx
}
}
}
Листинг 1. Сильная зависимость MyService от MyDataContext
The code above has drawbacks:
- the dictator antipattern is used: MyService itself creates and controls the lifetime of its dependency MyDataContext.
- the Dependency Inversion Principle (DIP) has been violated (where in the “science-like” article without SOLID): MyService depends on the specific implementation of MyDataContext, it would be better to use an interface / abstract class.
Dependency Inversion Principle (DIP)
In fact, a synonym for the requirement "Program in accordance with the interface, and not with a specific implementation."
(quote from the book)
Improving the code with aggregation - Listing 2:
class MyService
{
private readonly IRepository Repository;
public MyService(IRepository repository){
if(repository == null)
throw new ArgumentNullException(nameof(repository));
Repository = repository;
}
public void DoSomething()
{
// используем Repository
}
}
Листинг 2. Агрегация. MyService не создает и не управляет временем жизни свой зависимости Repository
Digression:
A good article about aggregation and composition was written by Sergei Teplyakov. Among other things, the article will teach you how to draw smart schemes. As a spoiler: which circuit describes aggregation?

Figure 1. Composition and aggregation schemes.
Let's return to listing 2. This is the introduction of dependency, and the best option is “Implementation of the designer”. Connectivity decreased, but the question arose: how to call the Dispose repository? Remember Listing 1 used Using?
A class that transfers control of its dependencies loses more than just the ability to choose specific implementations of abstractions. It also loses the ability to control both the instant of creating the instance and the moment when this instance becomes unavailable.
(quote from the book)
An interesting note: if a class has more than 4 dependencies (more than 4 constructor parameters) - this is an occasion to think about refactoring. It seems that the object performs too many functions, the Single Responsibility Principle (SRP - again SOLID) is violated.
Addiction Lifetime
Answering the question “how to call the Dispose repository?” Mark suggests to compromise. MyService should not be aware of the features of the IRepository implementation, including the need to free resources. Those. this definition of IRepository is undesirable:
interface IRepository : IDisposable
{
void DeleteProduct(int id);
}In addition to the fact that such an interface reveals to the consumer (MyService) some of the knowledge about a particular implementation, it also imposes a restriction on possible implementations - they must implement IDisposable (maybe they do not need it).
And in the implementation of IRepository, this knowledge about implementation is allowed - Listing 3.
class SqlRepository : IRepository
{
IDataContextFactory DbContextFactory;
public SqlRepository(IDataContextFactory dbContextFactory)
{
if(dbContextFactory == null)
throw new ArgumentNullException(nameof(dbContextFactory));
DbContextFactory = dbContextFactory;
}
public void DeleteProduct(int id);
{
using(var dbCtx = DbContextFactory.Create())
{
// использование dbCtx
}
}
}
Листинг 3. Реализация IRepository инкапсулирует работу с базой данных
Addition (not the most important): SqlRepository manages the lifetime of the DataConext, but the creation has been moved to the factory.
This is the trade-off: yes, SqlRepository controls the lifetime of the DataContext, but it does not affect the rest of the code.
A good solution is described above, but applying it is not always possible. For example, you need transactionality:
public void DoSomething(int productId)
{
this.Repository.DeleteProduct(productId);
this.Repository.DeleteHistory(productId);
}
Листинг 4. Удаление продукта и истории должно выполняться в одной транзакции
If the deletion of the history fails, the deletion of the product must be canceled (in the clever way this is the Unit of Work pattern). Then it is impossible to commit to the database separately in the DeleteProduct and DeleteHistory methods. How to be? You know where to look for the answer.
To be continued
We examined the main method of introducing envy: aggregation implemented using the implementation of the constructor. They touched on the topic of managing the lifetime of objects. See you again.