Estoy creando este post principalmente porque cuando hice el post y vídeo del patrón repositorio me preguntaron si podría crear un post que mostrará una abstracción de forma genérica del mismo.
Índice
Así que en post, lo que vamos a ver es cómo abstraer lo más posible en nuestro repositorio genérico.
1 - Qué debemos hacer común del patrón repositorio?
En este post no voy a pararme a explicar que es el patrón repositorio, sino que vamos a coger los elementos mas comunes y hacerlos comunes.
En circunstancias normales, o en una empresa, esta abstracción debería estar en un paquete NuGet común para toda la empresa si es posible.
Aquí lo que vamos a ver es que es lo más común y que puede ser compartido siempre, o casi siempre. Lo que viene siendo en la gran mayoría de casos, las operaciones CRUD, Crear, leer, actualizar y eliminar registros.
2 - Creando el repositorio genérico
Antes de empezar, no olvides que este post es parte de un curso más completo dentro de Entity Framework Core y que todo el código está en GitHub.
La forma más fácil de ver que es lo que podemos pasar a una parte genérica o común es con código que ya tenemos escrito, en el caso del curso es muy sencillo, pero en una aplicación más grande, tendríamos interfaces como esta muy similares en todos nuestros repositorios.
public interface IUserRepository
{
Task<User> Insert(User user);
Task<User?> GetById(int id);
Task<List<User>> GetAll();
void Update(User user);
Task<bool> Delete(int id);
}
Como puedes observar, tiene todos los apartados de CRUD que hemos mencionado antes.
2.1 - Creando la interfaz común
Así que eso es lo que vamos a crear para la interfaz de nuestro primer repositorio genérico.
Por supuesto, en vez de utilizar la entidad en cuestión, utilizaremos generics para pasar el tipo.
public interface IGenericRepository<T>
{
Task<T> Insert(T value);
Task<T?> GetById(int id);
IQueryable<T> GetAll();
void Update(T value);
Task<bool> Delete(int id);
}
Como puedes observar se ve prácticamente igual, pero hay dos cosillas a tener en cuenta.
La primera, el método GetAll
lo hemos cambiado de un tipo List
a un IQueryable
. Técnicamente podríamos utilizar IEnumerable
. Ambas opciones nos permiten realizar la consulta más adelante (deffer) . Pero cuando utilizamos IEnumerable
, si hacemos filtrado y ordenado de los datos, todas esas operaciones se van a hacer en memoria. mientras que con IQueryable
se van a traducir a SQL y va a ser la base de datos la encargada de hacer el “heavy lifting”, lo cual, va a ser más rápido y eficiente.
Podría hacer un post sobre el tema, pero creo que en este resumen está más que claro, usad IQueryable.
La segunda connotación, es en la búsqueda por Id, cada empresa lo va a hacer de una forma diferente, algunas simplemente van a utilizar siempre int
o Guid
para el tipo y pa’lante.
Otras, que yo creo que es lo más común, van a tener, en la propia entidad, una entidad base la cual contiene el ID así como otros datos comúnes, en nuestro caso, esos datos comunes puede ser el IsDeleted que vimos en el post de sobre soft delete, otras, van a tener una interfaz explícita para el ID.
En este post, simplemente vamos a incluirlo en la clase base, pasando un tipo genérico como Id.
public abstract class CursoEFBaseEntity<TId>
{
public TId Id { get; set; }
public bool IsDeleted { get; set; }
public DateTime? DeletedTimeUtc { get; set; }
}
En nuestro caso es siempre un entero, pero el código está preparado para cualquier tipo de dato.
Por supuesto, debemos implementar nuestro nuevo cambio en las entidades de nuestro proyecto
public class Wokringexperience : CursoEFBaseEntity<int>
{
...
}
public class User : CursoEFBaseEntity<int>
{
...
}
Ahora, lo que podemos hacer, es ir de vuelta a nuestra interfaz del repositorio genérico y especificar que T
tiene que ser parte del objeto que necesitamos, y eso implica añadir el tipo de dato de ID para así ser capaces de hacer tanto soft como hard deletes:
public interface IGenericRepository<T, TId>
where T : CursoEFBaseEntity<TId>
{
Task<T> Insert(T entity);
Task<T?> GetById(TId id);
IQueryable<T> GetAll();
void Update(T entity);
Task<bool> SoftDelete(TId id);
Task<bool> HardDelete(TId id);
}
Nota: como digo, este ejemplo soporta múltiples diferentes tipos de ID, pero si siempre vas a utilizar int o Guid, puedes ahorrarte el tipo genérico de ese caso.
2.2 - Implementando el repositorio común
El siguiente punto es Crear la implementación del repositorio genérico, simplemente creamos un GenericRepository
que herede de la interfaz que acabamos de crear e implementamos sus métodos. Por supuesto que no se te olvide inyectar el DbContext
, aunque bueno, se lo pasaremos de las clases que implementan a la clase abstracta.
public abstract class GenericRepository<T, TId> : IGenericRepository<T, TId>
where T : CursoEFBaseEntity<TId>
{
private readonly CursoEfContext _context;
protected DbSet<T> Entities => _context.Set<T>();
protected GenericRepository(CursoEfContext context)
{
_context = context;
}
public async Task<T> Insert(T entity)
{
EntityEntry<T> insertedValue = await _context.Set<T>().AddAsync(entity);
return insertedValue.Entity;
}
public async Task<T?> GetById(TId id)
=> await _context.Set<T>()
.FindAsync(id);
public IQueryable<T> GetAll()
=> _context.Set<T>();
public void Update(T entity)
{
_context.Set<T>().Update(entity);
}
public async Task<bool> SoftDelete(TId id)
{
T? entity = await GetById(id);
if (entity is null)
return false;
entity.IsDeleted = true;
entity.DeletedTimeUtc = DateTime.UtcNow;
_context.Set<T>().Update(entity);
return true;
}
public async Task<bool> HardDelete(TId id)
{
T? entity = await GetById(id);
if (entity is null)
return false;
_context.Set<T>().Remove(entity);
return true;
}
}
Cositas a tener en cuenta aquí:
Tenemos que especificar que T
es una clase en nuestro caso particular CursoEFBaseEntity<TId>
, técnicamente lo hemos hecho en la interfaz también, pero el motivo es porque necesitamos que sea una clase para que funcione, igual que entity framework, además de que si especificamos la clase en particular, tenemos acceso al IsDelted
para hacer el soft delete.
El uso de Set<T>
es porque Set devuelve un DbSet y si le pasamos el tipo, nos devuelve dicho DbSet, que traducido a Entity Framework y SQL, nos devuelve la tabla que necesitamos.
En nuestro caso, hemos simplificado el GetById
eliminando el Include original, que relacionaba mas de dos tablas, esto es muy común y normal, ya que a nivel del repositorio genérico no tenemos la información para saber que cargar.
La solución, si queremos mantener la funcionalidad original, sería, crear otro método en tu repositorio concreto que cargue todo lo necesario.
Finalmente, el uso de Find()
, Find lo que hace es ser capaz de entender múltiples tipos de datos para el Id, pero si por ejemplo, quieres si o si utilizar FirstOrDefault
o similares, no vas a poder hacer _context.set<T>().First(x=>x.id == id)
. Para ello lo que vas a tener que hacer es indicar que TId es comparable con IEquetable<Tid>
y en vez del ==
utilizar Equals
, aquí te lo muestro:
public abstract class GenericRepository<T, TId> : IGenericRepository<T, TId>
where T : CursoEFBaseEntity<TId>
where TId : IEquatable<TId> //<----- this line
{
...
public async Task<T?> GetById(TId id)
=> await _context.Set<T>()
.FirstAsync(a=>a.Id.Equals(id)); //<----- this line
}
2.3 - Aplicar el repositorio común
Lo que nos queda ahora es ir a los repositorios que tenemos ya generados, y actualizarlos para utilizar el repositorio común; En un proyecto real es esencial que llegados a este punto tengamos test, a ser posible de integración ya que debemos validar que todo sigue funcionando igual, y por ejemplo sabemos de primera mano que hemos cambiando el GetAll, ya que ya no incluye los objetos relacionados.
Lo primero es ir a la interfaz, y actualizarla para utilizar la nueva:
public interface IUserRepository : IGenericRepository<User, int>
{
Task<User?> GetByIdWithWorkingExperiences(int id);
}
Vemos que apenas cambia nada a nivel general, pero hemos reducido mucho el código, únicamente debemos cambiar el GetById
para que devuelva las workingExperiences
.
Y ahora aplicamos el cambio a la implementación del repositorio:
public class UserRepository : GenericRepository<User, int>, IUserRepository
{
public UserRepository(CursoEfContext context) : base(context)
{
}
public async Task<User?> GetByIdWithWorkingExperiences(int id)
=> await Entities
.Include(a => a.Wokringexperiences)
.FirstOrDefaultAsync(x => x.Id == id);
}
Con este cambio hemos centralizado todas las operaciones comunes dentro de un repositorio y a la larga reducido en gran medida el número de líneas que mantener.