The Dependency Injection is a pattern for receiving objects instead of creating them inside a class.
In the previous article, we saw what services are and why they help us clean up the code. But we were left with a problem: we were still manually doing new MiServicio().
Let’s solve that by using one of the most important tools in ASP.NET Core: the dependency injection container.
What problem does it solve
Without dependency injection, a class creates everything it needs:
public class UserService
{
private readonly EmailService _emailService = new EmailService();
}This seems convenient at first, but it creates tight coupling. UserService becomes glued to EmailService, and changing it for another implementation or testing it becomes more cumbersome.
With dependency injection, the class asks for what it needs:
public class UserService
{
private readonly IEmailService _emailService;
public UserService(IEmailService emailService)
{
_emailService = emailService;
}
}Now UserService doesn’t know who creates IEmailService. It only knows that someone delivers it ready to use.
The service container
In ASP.NET, we register services in Program.cs, before calling builder.Build().
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IEmailService, EmailService>();
var app = builder.Build();With this registration, we tell .NET:
When someone asks for
IEmailService, give them anEmailService.
And that’s it. No more ceremony.
Injecting a service
In Minimal APIs, we can request the service as an endpoint parameter:
app.MapPost("/registro", (
UsuarioDto user,
IUserService userService) =>
{
userService.Register(user);
return Results.Ok();
});In controllers, the usual approach is to inject it into the constructor:
[ApiController]
[Route("api/users")]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
public UsersController(IUserService userService)
{
_userService = userService;
}
[HttpPost]
public IActionResult Create(UsuarioDto user)
{
_userService.Register(user);
return Ok();
}
}The controller stays focused on HTTP, and the service handles the logic.
Lifecycles
When registering a service, we must choose how long the created instance lives.
| Method | Instance Created | Typical Usage |
|---|---|---|
AddTransient | Every time it is requested | Lightweight, stateless utilities |
AddScoped | Once per HTTP request | Business services, repositories, DbContext |
AddSingleton | Once per application | Caches, configuration, safe shared services |
AddTransient() creates a new instance every time someone requests the service.
builder.Services.AddTransient<ITextFormatter, TextFormatter>();It is useful for small, stateless, and cheap to create services.
AddScoped() creates an instance per HTTP request.
builder.Services.AddScoped<IUserService, UserService>();This is the most common lifecycle in web applications. During the same request, the same instance is reused, but another request gets its own.
That’s why it fits so well with DbContext: we want to share it within an operation, but not mix data between different users.
AddSingleton() creates a single instance for the entire application.
builder.Services.AddSingleton<ICacheService, CacheService>();It works well for shared, concurrency-safe objects. But it must be used with care.
Do not store mutable user state inside a singleton. All users share the same instance, so an internal variable can become quite an awkward party.
There is also an important rule: we should not inject Scoped services into Singleton services. A singleton lives for the entire application, while a scoped service belongs to a specific request.
Interfaces and Implementations
The norm is to register an interface and an implementation:
builder.Services.AddScoped<IUserService, UserService>();This way, the code consumes IUserService, not UserService directly.
This allows us to change the implementation without touching the consuming code. For example, in tests, we can use a FakeUserService or a mock.
builder.Services.AddScoped<IUserService, FakeUserService>();This separation is one of the foundations for doing unit tests without having to boot up half an application.