Telerik blogs

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.

What is Dependency Injection and What are Its Advantages?

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:

  • Decoupling: DI reduces coupling between application components, allowing them to be modified independently, making system maintenance and evolution easier.
  • Code reuse: DI enables component sharing, which promotes code reuse and avoids unnecessary duplication.
  • Testability: DI facilitates writing unit tests, as it allows for easy replacement of dependencies with mocks or fake implementations during the construction and execution of tests.

The schema below demonstrates how ASP.NET Core handles DI.

di-schema

Practicing with 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.

Prerequisites

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.

Implementing the IoC

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:

  1. 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>();
  1. 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>();
  1. 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:

Accessing the data

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:

  • Tight coupling: The 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.
  • Testability difficulty: By manually creating the dependency, it becomes difficult to perform efficient unit tests. Tests can depend directly on the actual implementation of the repository, making them more complex and less reliable.
  • Complex maintenance: Without DI, adding or replacing dependencies requires direct changes to the source code of the classes that use them. This increases the risk of introducing bugs and makes code maintenance more complex and problem-prone.
  • Limited reusability: Without DI, it’s not easy to reuse the same repository instance across multiple classes or components. Each class would have to create its own instance separately, leading to unnecessary code duplication.

What’s the Relationship between DI and DIP?

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.

The Service Locator Pattern

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:

  • Tight coupling: The Service Locator creates a tight coupling between the classes that use it and the Service Locator, making it difficult to replace and test these dependencies.
  • Difficulty in the configuration: Service Locator requires explicit configuration to register and configure the services in the locator, which can be more labor-intensive and error-prone.
  • Lack of transparency: Service Locator does not make the dependencies of a class explicit, making the code less understandable and more difficult to maintain.

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.

Conclusion

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.


assis-zang-bio
About the Author

Assis Zang

Assis Zang is a software developer from Brazil, developing in the .NET platform since 2017. In his free time, he enjoys playing video games and reading good books. You can follow him at: LinkedIn and Github.

Related Posts

Comments

Comments are disabled in preview mode.