Project – Dependency inversion – Architectural Principles
In this section, we translate the preceding iteration of the inverted dependency example in code. We create the following assemblies to align with the preceding diagram:
- App is a console application that references all projects to showcase different use cases.
- Core is a class library that depends on the Abstractions package.
- Abstractions is a class library that contains the IDataPersistence interface.
- Sql and Local are class libraries that reference the Abstractions project and implement the IDataPersistence interface.
The code sample has few implementation details because it is irrelevant to understanding the DIP. Please assume we implemented the Persist methods logic using your favorite in-memory and SQL databases.
Visually, the relationships between the packages look like the following:
Figure 3.5: the visual representation of the packages and their relationships
Code-wise, our abstraction contains a Persist method that we use to showcase the DIP:
namespace Abstractions;
public interface IDataPersistence
{
void Persist();
}
Next, the LocalDataPersistence class depends on the Abstractions package and outputs a line to the console, allowing us to trace what happens in the system:
using Abstractions;
namespace Local;
public class LocalDataPersistence : IDataPersistence
{
public void Persist()
{
Console.WriteLine(“Data persisted by LocalDataPersistence.”);
}
}
Next, the SqlDataPersistence class is very similar to the LocalDataPersistence class; it depends on the Abstractions package and outputs a line in the console, allowing us to trace what happens in the system:
using Abstractions;
namespace Sql;
public class SqlDataPersistence : IDataPersistence
{
public void Persist()
{
Console.WriteLine(“Data persisted by SqlDataPersistence.”);
}
}
Before we get to the program flow, we still have the SomeService class to look at, which depends on the Abstractions package:
using Abstractions;
namespace App;
public class SomeService
{
public void Operation(IDataPersistence someDataPersistence)
{
Console.WriteLine(“Beginning SomeService.Operation.”);
someDataPersistence.Persist();
Console.WriteLine(“SomeService.Operation has ended.”);
}
}
The highlighted code shows that the SomeService class calls the Persist method of the provided IDataPersistence interface implementation. The SomeService class is not aware of where the data go. In the case of full implementation, the someDataPersistence instance is responsible for where the data would be persisted. Other than that, the Operation method writes lines to the console so we can trace what happens. Now from the App package, the Program.cs file contains the following code:
using Core;
using Local;
using Sql;
var sqlDataPersistence = new SqlDataPersistence();
var localDataPersistence = new LocalDataPersistence();
var service = new SomeService();
service.Operation(localDataPersistence);
service.Operation(sqlDataPersistence);
In the preceding code, we create a SqlDataPersistence and a LocalDataPersistence instance. Doing that forced us to depend on both packages, but we could have chosen otherwise.Then we create an instance of the SomeService class. We then pass both IDataPersistence implementations to the Operation method one after the other.When we execute the program we get the following output:
Beginning SomeService.Operation.
Data persisted by LocalDataPersistence.
SomeService.Operation has ended.
Beginning SomeService.Operation.
Data persisted by SqlDataPersistence.
SomeService.Operation has ended.
The first half of the preceding terminal output represents the first call to the Operation method, where we passed the LocalDataPersistence instance. The second half represents the second call, where we passed the SqlDataPersistence instance.The highlighted lines show that depending on an interface allowed us to change this behavior (OCP). Moreover, we could create a CosmosDb package, reference it from the App package, then pass an instance of a CosmosDbDataPersistence class to the Operation method, and the Core package would not know about it. Why? Because we inverted the dependency flow, creating a loosely coupled system. We even did some dependency injection.
Dependency injection, or Inversion of Control (IoC), is a design principle that is a first-class citizen of ASP.NET Core. It allows us to map abstractions to implementations, and when we need a new type, the whole object tree gets created automatically based on our configuration. We start that journey in Chapter 7, Dependency Injection.