A service in ASP.NET Core is a class that encapsulates a specific responsibility of the application.
In the world of ASP.NET Core, you’ll hear the word “service” all the time. “Inject the service”, “register the service”, “service layer”… and at first, it seems like everything is a service and nothing makes sense.
Fortunately, the idea is quite simple: a service is a piece of code that knows how to do one specific thing. It can contain business logic, data access, calls to external APIs, or reusable tasks.
What is a service
A service is nothing particularly special. It doesn’t need to inherit from a special base class or have a magic attribute on top.
It can be a normal class:
public class EmailService
{
public void SendWelcomeEmail(string email)
{
// Send welcome email
}
}What matters is not the name, but the responsibility. A service exists to take work out of the controller or endpoint and put each piece in its place.
- The controller receives the HTTP request and returns a response.
- The service executes the specific logic that the application needs.
The problem of the all-purpose controller
Suppose we are creating an API to register users. We could write everything directly in the endpoint:
app.MapPost("/registro", (UsuarioDto usuario) =>
{
if (string.IsNullOrEmpty(usuario.Email))
{
return Results.BadRequest();
}
var passwordHash = CalcularHash(usuario.Password);
using var db = new MiDbContext();
db.Usuarios.Add(new Usuario
{
Email = usuario.Email,
Password = passwordHash
});
db.SaveChanges();
var smtp = new SmtpClient("smtp.example.com");
smtp.Send("[email protected]", usuario.Email, "Hola", "Bienvenido");
return Results.Ok();
});This can work, of course. But the endpoint is doing too many things: it validates, calculates, saves, and sends emails.
If the email provider changes tomorrow, we modify the endpoint. If we want to register users from another part of the application, we duplicate code. And if we want to test this logic, we drag half the application along with it.
Separating responsibilities
A cleaner version would be to separate that logic into services:
public class UserService
{
public void Register(UsuarioDto usuario)
{
// Validate business rules
// Calculate hash
// Save user
}
}And another service for email:
public class EmailService
{
public void SendWelcomeEmail(string email)
{
// Send welcome email
}
}Then the endpoint becomes much smaller:
app.MapPost("/registro", (UsuarioDto usuario) =>
{
var userService = new UserService();
var emailService = new EmailService();
userService.Register(usuario);
emailService.SendWelcomeEmail(usuario.Email);
return Results.Ok();
});There is still something to improve, because we are still using new. But the first step is already done: the logic has been separated into reusable pieces.
Custom services and framework services
In ASP.NET Core, there are services we create ourselves, like UserService, and services that already come from the framework or external packages.
Some common examples are:
| Service | Purpose |
|---|---|
ILogger<T> | Write structured logs |
IConfiguration | Read configuration |
IWebHostEnvironment | Know the current environment |
IOptions<T> | Use typed configuration |
IHttpContextAccessor | Access HttpContext outside the controller |
Not all of them are always available. Some are registered automatically, and others need to be activated using methods like AddAuthentication(), AddAuthorization(), AddMemoryCache(), or AddHttpContextAccessor().
Stateless and stateful services
In web applications, we usually want our services to be stateless. That is, they should not store their own information between requests.
public class PaymentService
{
private readonly IPaymentGateway _gateway;
public PaymentService(IPaymentGateway gateway)
{
_gateway = gateway;
}
public Task<PaymentResult> ProcessAsync(PaymentRequest request)
{
return _gateway.ChargeAsync(request.Amount, request.Currency);
}
}Each call receives the data it needs and returns a result. This makes the service easier to test, reuse, and scale.
You have to be careful when saving user data in internal fields of a service. Depending on the lifecycle with which it is registered, we could unintentionally share state between requests.