Un servicio en ASP.NET Core es una clase que encapsula una responsabilidad concreta de la aplicación.
En el mundo de ASP.NET Core vas a escuchar la palabra “servicio” cada dos por tres. “Inyecta el servicio”, “registra el servicio”, “capa de servicios”… y al principio parece que todo es un servicio y nada tiene sentido.
La idea, por suerte, es bastante sencilla: un servicio es una pieza de código que sabe hacer una cosa concreta. Puede contener lógica de negocio, acceso a datos, llamadas a APIs externas o tareas reutilizables.
Qué es un servicio
Un servicio no es nada especialmente raro. No tiene que heredar de una clase base especial ni tener un atributo mágico encima.
Puede ser una clase normal:
public class EmailService
{
public void SendWelcomeEmail(string email)
{
// Enviar email de bienvenida
}
}Lo importante no es el nombre, sino la responsabilidad. Un servicio existe para sacar trabajo del controlador o del endpoint y dejar cada pieza en su sitio.
- El controlador recibe la petición HTTP y devuelve una respuesta.
- El servicio ejecuta la lógica concreta que necesita la aplicación.
El problema del controlador todoterreno
Supongamos que estamos creando una API para registrar usuarios. Podríamos escribirlo todo directamente en el 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();
});Esto puede funcionar, claro. Pero el endpoint está haciendo demasiadas cosas: valida, calcula, guarda y envía correos.
Si mañana cambia el proveedor de correo, tocamos el endpoint. Si queremos registrar usuarios desde otra parte de la aplicación, duplicamos código. Y si queremos probar esta lógica, nos llevamos media aplicación detrás.
Separar responsabilidades
Una versión más limpia sería separar esa lógica en servicios:
public class UserService
{
public void Register(UsuarioDto usuario)
{
// Validar reglas de negocio
// Calcular hash
// Guardar usuario
}
}Y otro servicio para el correo:
public class EmailService
{
public void SendWelcomeEmail(string email)
{
// Enviar email de bienvenida
}
}Entonces el endpoint queda mucho más pequeño:
app.MapPost("/registro", (UsuarioDto usuario) =>
{
var userService = new UserService();
var emailService = new EmailService();
userService.Register(usuario);
emailService.SendWelcomeEmail(usuario.Email);
return Results.Ok();
});Todavía hay algo que mejorar, porque seguimos usando new. Pero el primer paso ya está: la lógica se ha separado en piezas reutilizables.
Servicios propios y servicios del framework
En ASP.NET Core hay servicios que creamos nosotros, como UserService, y servicios que ya vienen del framework o de paquetes externos.
Algunos ejemplos habituales son:
| Servicio | Para qué sirve |
|---|---|
ILogger<T> | Escribir logs estructurados |
IConfiguration | Leer configuración |
IWebHostEnvironment | Saber el entorno actual |
IOptions<T> | Usar configuración tipada |
IHttpContextAccessor | Acceder al HttpContext fuera del controlador |
No todos están disponibles siempre. Algunos se registran automáticamente y otros hay que activarlos con métodos como AddAuthentication(), AddAuthorization(), AddMemoryCache() o AddHttpContextAccessor().
Servicios con y sin estado
En aplicaciones web, normalmente queremos que nuestros servicios sean sin estado. Es decir, que no guarden información propia entre peticiones.
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);
}
}Cada llamada recibe los datos que necesita y devuelve un resultado. Eso hace que el servicio sea más fácil de probar, reutilizar y escalar.
Hay que tener cuidado al guardar datos de usuario en campos internos de un servicio. En función del ciclo de vida con el que se registre, podríamos compartir estado entre peticiones sin querer.