Check out what happens if you don’t use dependency injection, and then level up with best practices around DI, IoC and DIP.
Dependency injection (DI) is a design pattern widely used in software development, and understanding it is a basic requirement for anyone wanting to work with web development in ASP.NET Core. The purpose of DI is to promote code modularity, reusability and testability. In ASP.NET Core specifically, DI plays a crucial role in building robust and scalable applications.
This post explains the concept of DI simply, describes the relationships between DI, Inversion of Control (IoC), Dependency Inversion Principle (DIP) and Service Locator, and demonstrates how to implement DI, complete with code samples.
DI is a concept that allows external actors, such as construction parameters, properties or configuration methods, to provide dependencies of a class rather than creating them within the class. This reduces coupling between system components, making the code more stable and modular.
Using dependency injection in ASP.NET Core, you can gain several advantages, including:
The schema below demonstrates how ASP.NET Core handles DI.
Next, let’s create a simple application in ASP.NET Core to demonstrate how to implement DI. Then we’ll check out the same example, but without dependency injection and see what problems this can bring.
To create the example in this post, you need to install the .NET SDK, version 7 or newer.
You also need a terminal to run .NET commands. You can run them directly from an IDE if you prefer; this example uses Visual Studio Code.
You can access the source code of this post’s examples on GitHub.
To create the base application, run the following command in the terminal:
dotnet new web -o ContactRegister
Open the newly created project with your favorite IDE. In the project, create a new folder called Models
and inside it create a new file called Contact.cs
. Replace the existing code with the code below:
namespace ContactRegister.Models;
public record Contact(Guid Id, string Name, string Email, string PhoneNumber);
Create a new folder called Data
and inside it create a new interface called IContactRepository.cs
. Put the code below in it:
using ContactRegister.Models;
namespace ContactRegister.Repository;
public interface IContactRepository{
public List<Contact> FindContacts();
}
Still inside the Data
folder, create a new class called ContactRepository.cs
and put the code below in it:
using ContactRegister.Models;
namespace ContactRegister.Repository;
public class ContactRepository : IContactRepository
{
public List<Contact> FindContacts(){
var contacts = new List<Contact>(){
new Contact(Guid.NewGuid(), "John Smith", "jsmith@examplemail.com", "987654321"),
new Contact(Guid.NewGuid(), "Amy Davis", "amy@examplemail.com", "987654321")
};
return contacts;
}
}
Note that in the previous code, you created a record to represent the contact entity, then you created an interface and a class that has a method that returns a list of contacts.
Now, create a new folder called Services
. Inside it, create a new class called ContactService.cs
and put in the code below:
using ContactRegister.Repository;
using ContactRegister.Models;
namespace ContactRegister.Services;
public class ContactService {
private readonly IContactRepository _repository;
public ContactService(IContactRepository repository)
{
_repository = repository;
}
public List<Contact> FindAllContacts() =>
_repository.FindContacts();
}
Note that in the code above, in order to use the FindContacts()
method of the ContactRepository
class, you are injecting the dependency through the declaration of the class private readonly IContactRepository _repository;
. You are then passing the class ContactRepository
in the constructor of the class ContactService
, like so:
public ContactService(IContactRepository repository)
{
_repository = repository;
}
This way, every time the ContactService
class is instantiated, a new instance of the ContactRepository
class will be created and will be available for use.
In ASP.NET Core, Inversion of Control (IoC) is a design pattern where the responsibility for creating and managing objects is transferred to an IoC container, rather than being controlled directly by application code.
IoC promotes decoupling and modularity in application development. Rather than a class directly depending on other classes or instantiating objects directly, it declares its dependencies through interfaces or abstract base classes. The IoC container is responsible for resolving these dependencies and providing the necessary implementations.
In newer versions of ASP.NET Core, you can configure the IoC container through the Program
class. You can register your application’s dependencies using the AddTransient
, AddScoped
and AddSingleton
methods, depending on the required lifecycle for each service.
The IoC container manages the creation of these objects and ensures that dependencies are correctly resolved. Using IoC, we’re delegating the responsibility of dealing with the DI to ASP.NET Core native resources rather than doing it manually.
To implement IoC in your app, add the code below in the Program.cs
file:
builder.Services.AddSingleton<IContactRepository, ContactRepository>();
Note that in the above code, you’re passing the ContactRepository
class and the IContactRepository
interface to the AddSingleton
extension. This is one of the ways to implement dependency injection in ASP.NET Core.
In the .NET ecosystem, there are three main forms supported by ASP.NET Core’s native dependency injection framework:
AddSingleton
: This method registers a dependency as a singleton. This means that a single instance of the service will be created and used by the entire application. Example:builder.Services.AddSingleton<IContactRepository, ContactRepository>();
AddScoped
: This method registers a scoped dependency. It ensures that a single instance of the service is created and used for the lifetime of a request. This means that each request receives a different instance of the dependency. Example:builder.Services.AddScoped<IContactRepository, ContactRepository>();
AddTransient
: This method registers a dependency as transient. This means that a new instance of the service is created each time it’s requested. Example:builder.Services.AddTransient<IContactRepository, ContactRepository>();
To make the API functional, you just need to create an endpoint to access the data. Still in the Program.cs
file, add the code below:
app.MapGet("/contacts", (IContactRepository repository) => {
var contacts = repository.FindContacts();
return Results.Ok(contacts);
});
If you run the command dotnet run
in the terminal and access the address http://localhost:PORT
in your browser, you’ll get the following result:
Note that the dependency injection worked, and you’re able to access the data.
Now, let’s see what it would be like if you did the same thing but without using dependency injection. In this case, the ContactService
class would look like this:
public class ContactService
{
private readonly IContactRepository _repository;
public ContactService()
{
_repository = new ContactRepository(); // Manual dependency creation
}
// ...
}
Note that this way, instead of passing the instance of the ContactRepository
class in the constructor of the service class, a new instance of the ContactRepository
class is created manually through the new
operator.
This practice is wrong and should not be used as it can cause several problems such as:
ContactClass
class is tightly coupled to the concrete repository implementation. This makes it difficult to replace the implementation with another one without directly modifying the class code. This limits the flexibility and extensibility of the system.The Dependency Inversion Principle (DIP) refers to one of the principles of SOLID, a set of software design guidelines that promotes code modularity, flexibility and maintainability. DIP states that high-level classes should not directly depend on low-level classes. Instead, they must rely on abstractions.
In the context of ASP.NET Core, DIP implementation is achieved through the use of interfaces or abstract classes to define contracts and abstractions. Instead of high-level classes directly depending on low-level classes, they rely on interfaces or abstract classes that represent these dependencies.
ASP.NET Core uses dependency injection (DI) to implement DIP. As discussed earlier, it’s through DI that dependencies are injected into classes at runtime, rather than being created or instantiated directly in code. This promotes loose coupling between classes and makes replacing implementations easier; you can easily provide different implementations of a dependency without modifying the code that uses it.
In short, DIP in ASP.NET Core is achieved by applying the principle of inverting dependencies through the use of DI.
When you talk about DI in languages like C# and Java, you’re going to run across the term Service Locator a lot.
Service Locator is an old design pattern that allows you to get instances of services through a centralized locator. Although it was used in some older applications and frameworks, it has some disadvantages compared to DI:
In ASP.NET Core, Service Locator is neither an officially supported design pattern nor recommended by the framework. The recommended approach to dependency resolution in ASP.NET Core is dependency injection.
As we saw in this post, ASP.NET Core has a robust native dependency injection engine that offers advanced features such as lifecycle control, service configuration and support for service abstraction.
Microsoft documentation recommends avoiding the use of the Service Locator pattern.
In short, Service Locator can be useful in specific scenarios where it’s not possible to use DI, such as with legacy code or dynamic configuration of services, but it’s always preferable to use dependency injection.
At first glance, dependency injection may seem like a complex and difficult subject, but as shown throughout the post, it’s possible to implement DI in a simple way, using only native features of ASP.NET Core.
Every developer working with object-oriented languages such as C# should understand how DI. Its implementation will be common in their work routine, especially when creating any application using ASP.NET Core.