Resumen

Crear una capa de servicios sobre Entity Framework aporta orden, testabilidad y claridad. Aquí verás cómo definir interfaces, implementar métodos async con await, optimizar lecturas con AsNoTracking y registrar los servicios en Program.cs usando un ciclo de vida adecuado.

¿Por qué separar en servicios con AppDbContext y controllers?

Diseñar una capa intermedia evita que el controller consuma directamente el AppDbContext. Así, la lógica de acceso a datos se concentra en servicios inyectables por dependencia, facilitando mantenimiento y pruebas.

  • Capas claras: controlador expone datos, servicio ejecuta lógica, contexto persiste cambios.
  • Inyección por constructor: recibes AppDbContext en el servicio y lo asignas a un campo privado.
  • Buenas prácticas: nombres de métodos con sufijo Async cuando son asíncronos.

¿Qué métodos async definen IUserService e ITaskService?

Ambas interfaces comparten contratos genéricos, cambiando solo el tipo de modelo: User o TaskItem. Los nombres no incluyen el tipo (p. ej., no usar getUserById), lo que simplifica la implementación.

  • GetAllAsync: devuelve toda la lista como lectura.
  • GetByIdAsync: devuelve un elemento o null si no existe.
  • CreateAsync: crea y retorna el elemento creado.

Ejemplo de interfaces:

public interface IUserService { Task<IEnumerable<User>> GetAllAsync(); Task<User?> GetByIdAsync(int id); Task<User> CreateAsync(User user); } public interface ITaskService { Task<IEnumerable<TaskItem>> GetAllAsync(); Task<TaskItem?> GetByIdAsync(int id); Task<TaskItem> CreateAsync(TaskItem item); }

¿Cómo implementar UserService y TaskService con buenas prácticas?

Primero, inyecta AppDbContext por el constructor. Luego, implementa los métodos usando await y persistiendo cambios con SaveChangesAsync. Para lecturas, favorece AsNoTracking y métodos asíncronos como FirstOrDefaultAsync.

  • Inyección por constructor: patrón consistente, similar al logger usado en WeatherForecastController.
  • CreateAsync: usar Add y luego SaveChangesAsync, retornando el objeto creado.
  • GetAllAsync: lista completa con AsNoTracking() cuando no modificas el contexto.
  • GetByIdAsync: FirstOrDefaultAsync para obtener el primer elemento o null.
  • Nullabilidad: User? y TaskItem? para permitir null cuando no existe el ID buscado.

Ejemplo de implementación:

public class UserService : IUserService { private readonly AppDbContext _context; public UserService(AppDbContext context) => _context = context; public async Task<User> CreateAsync(User user) { _context.Users.Add(user); await _context.SaveChangesAsync(); return user; } public async Task<IEnumerable<User>> GetAllAsync() => await _context.Users.AsNoTracking().ToListAsync(); public async Task<User?> GetByIdAsync(int id) => await _context.Users.FirstOrDefaultAsync(u => u.Id == id); } public class TaskService : ITaskService { private readonly AppDbContext _context; public TaskService(AppDbContext context) => _context = context; public async Task<TaskItem> CreateAsync(TaskItem item) { _context.TaskItems.Add(item); await _context.SaveChangesAsync(); return item; } public async Task<IEnumerable<TaskItem>> GetAllAsync() => await _context.TaskItems.AsNoTracking().ToListAsync(); public async Task<TaskItem?> GetByIdAsync(int id) => await _context.TaskItems.FirstOrDefaultAsync(t => t.Id == id); }

¿Cómo optimizar lecturas con AsNoTracking?

Cuando no vas a modificar entidades, no necesitas tracking. Usar AsNoTracking() en listas de solo lectura elimina costos de seguimiento y mejora el rendimiento en queries de lectura.

¿Cómo manejar null con tipos anulables?

GetByIdAsync puede no encontrar el elemento: devuelve null. Marca el tipo como anulable (User?, TaskItem?) para reflejar esa posibilidad y evitar errores en tiempo de compilación.

¿Cómo registrar los servicios en Program.cs y elegir el ciclo de vida?

La configuración se agrega junto a otros servicios: login, DB context, etc. Se sugiere registrar con scope y conocer alternativas: singleton y "traction".

  • scope: instancia por petición, estándar en acceso a datos.
  • singleton: una única instancia en todo el ciclo de vida.
  • "traction": crea una nueva instancia por cada llamado.
  • Nota: se recomienda scope salvo casos donde se inyecte por parámetros y por constructor al mismo tiempo.

Ejemplo de registro:

// En Program.cs services.AddScoped<IUserService, UserService>(); services.AddScoped<ITaskService, TaskService>();

Siguiente paso: completa los métodos pendientes Update y Delete para ambos servicios y piensa cómo mapearlos en el controller. ¿Qué casos borde tendrás en update y cómo manejarás null en delete?

Si tienes dudas o quieres compartir tu solución de update/delete, deja un comentario y comencemos la conversación.