Resumen

Separar la lógica de acceso a datos del controlador es una de las mejores decisiones de arquitectura que puedes tomar en un proyecto .NET. Cuando creas servicios en .NET con Entity Framework, evitas que el controlador hable directamente con el AppDbContext y ganas una capa intermedia limpia, testeable y reutilizable. Esta guía es para quienes ya tienen un contexto y modelos listos y quieren dar el siguiente paso.

Por qué necesitas una capa de servicios entre el controlador y la base de datos

El controlador debería exponer datos, no contenerlos. Si metes la lógica de Entity Framework dentro del controlador, terminas con clases enormes, difíciles de probar y acopladas al ORM.

La capa de servicios resuelve eso. El controlador llama al servicio, el servicio habla con el AppDbContext, y cada pieza tiene una sola responsabilidad. Esto es lo que se conoce como separation of concerns, y es la base para aplicar inyección de dependencias correctamente [02:10].

¿Qué hace un servicio en .NET? Es una clase que encapsula la lógica de negocio o de acceso a datos. El controlador la recibe como dependencia inyectada, sin saber cómo está implementada por dentro.

Cómo defines la interfaz IUserService antes de implementarla

La regla es simple: primero la interfaz, después la clase. Trabajar contra interfaces te permite inyectar la dependencia sin que el controlador conozca la implementación concreta.

Dentro de una carpeta Services, creas IUserService con tres métodos asíncronos:

  • Task<IEnumerable<User>> GetAllAsync() para devolver toda la lista.
  • Task<User?> GetByIdAsync(int id) para devolver un usuario por id, permitiendo null.
  • Task<User> CreateAsync(User user) para crear y devolver el usuario creado.

Dos detalles importantes. El sufijo Async se añade solo a métodos asíncronos, es una convención que ayuda a leer el código rápido [04:20]. Y el User? con interrogación habilita los nulos, porque puede que el id buscado no exista en la base de datos [05:15].

Cómo implementas UserService inyectando el AppDbContext por constructor

La clase UserService implementa IUserService y recibe el AppDbContext por el constructor. Ese contexto se guarda en una propiedad privada y queda disponible para todos los métodos.

csharp private readonly AppDbContext _context;

public UserService(AppDbContext context) { _context = context; }

Es la misma mecánica que ya viste con el logger en WeatherForecastController: declaras la dependencia y la recibes en el constructor [06:40].

Cómo escribes GetAllAsync, GetByIdAsync y CreateAsync

Cada método usa el contexto de Entity Framework de forma directa pero asíncrona:

  • GetAllAsync: hace return await _context.Users.AsNoTracking().ToListAsync();. El AsNoTracking mejora el rendimiento en consultas de solo lectura porque le dice a EF que no rastree cambios sobre esas entidades [08:05].
  • GetByIdAsync: usa FirstOrDefaultAsync con la condición del id. Si no encuentra el elemento, devuelve null, que es el valor por defecto del tipo.
  • CreateAsync: ejecuta _context.Users.Add(user), luego await _context.SaveChangesAsync() y finalmente retorna el usuario.

El SaveChangesAsync es obligatorio después de cualquier cambio sobre el contexto. Sin él, los datos no se persisten en la base. Y recuerda marcar el método como async para poder usar await, un detalle que Visual Studio a veces olvida cuando autogenera la implementación [07:30].

¿Para qué sirve AsNoTracking en Entity Framework? Para acelerar consultas de lectura. Le indica a EF Core que no necesita rastrear los cambios de las entidades devueltas, lo que reduce memoria y mejora velocidad.

Cómo replicas el mismo patrón en ITaskService y TaskService

El servicio de tareas sigue exactamente la misma estructura. Por eso conviene mantener nombres genéricos como GetById y GetAll en lugar de GetUserById o GetTaskById. Así puedes copiar la interfaz y solo cambiar el tipo.

Un detalle que confunde: el modelo se llama TaskItem, no Task. Si escribes Task a secas, .NET lo interpreta como System.Threading.Tasks.Task, la tarea de un hilo, y rompe la compilación [12:00]. Asegúrate de usar TaskItem en todas las firmas.

Dentro de TaskService la lógica es idéntica a UserService: contexto inyectado por constructor, FirstOrDefaultAsync para búsqueda por id, Add más SaveChangesAsync para crear.

Cómo registras los servicios en Program.cs con AddScoped

Para que la inyección de dependencias funcione, debes registrar los servicios en Program.cs:

csharp builder.Services.AddScoped<IUserService, UserService>(); builder.Services.AddScoped<ITaskService, TaskService>();

Ahí aparece la pregunta clave: ¿AddScoped, AddSingleton o AddTransient?

  • Singleton: una única instancia durante todo el ciclo de vida de la aplicación.
  • Transient: una nueva instancia en cada llamada al servicio.
  • Scoped: una instancia por request HTTP.

Si buscas una aplicación 100% stateless, Transient es la opción más segura. En la práctica, Scoped es el estándar porque la mayoría de las apps inyectan dependencias solo por constructor y nunca chocan instancias [14:45].

¿Cuándo uso AddScoped en lugar de AddTransient? Usa Scoped cuando quieres que el servicio comparta la misma instancia durante una request HTTP completa, como ocurre con DbContext. Usa Transient si necesitas una instancia nueva en cada inyección.

Qué te falta para terminar el CRUD completo

Con GetAll, GetById y Create cubres tres de las cuatro operaciones básicas. Te falta UpdateAsync y DeleteAsync tanto en IUserService como en ITaskService, junto con sus implementaciones. Piensa también cómo vas a mapear estos métodos al controlador para exponerlos como endpoints.

¿Ya intentaste implementar el Update y el Delete? Cuéntame en los comentarios qué decisión tomaste con el manejo de nulos cuando el id no existe.

      Servicios en .NET con Entity Framework