EF-Core-Database

Patrón Repositorio & Unit of Work con Entity Framework Core y .NET 6

En este artículo, mostraré una breve introducción sobre el patrón Repositorio y el patrón Unit of Work. También veremos un ejemplo práctico de cómo implementar estos repositorios con Entity Framework Core y .Net 6

Introducción a patrones Repositorio & Unit of Work

¿Qué es el patrón repositorio?

Un repositorio no es más que una clase con código de persistencia de datos coordinada por una unidad de trabajo (DBContext en EF Core). Esta clase contiene todas las operaciones posibles sobre esa persistencia específica.

Por lo tanto, a nuestra aplicación no le importará qué tipo de ORM estamos usando, ya que todo lo relacionado con el ORM se maneja dentro de una capa de repositorio. Esto nos permite tener una separación más limpia de preocupaciones. El patrón repositorio es uno de los patrones de diseño más utilizados para crear soluciones más limpias.

También puede haber casos en los que necesitemos utilizar varios ORM en una única solución. Probablemente Dapper para obtener los datos y EFCore para escribir los datos. Esto es únicamente para optimizaciones de rendimiento.

El patrón repositorio nos ayuda a lograr esto mediante la creación de una abstracción sobre la capa de acceso a datos. Ahora, ya no tenemos que depender de EF Core ni de ningún otro ORM para nuestra aplicación.

¿Por qué queremos usar patrón repositorio?

Hay muchas razones por las que usar este patrón:

  • Reducir la duplicación de código.
  • Acoplamiento débil a la tecnología de persistencia subyacente.
  • La capacidad de prueba es mucho más fácil, el patrón repositorio nos permitirá simular nuestra base de datos para que podamos realizar nuestras pruebas.
  • Separación de preocupaciones, funcionalidades de aplicación separadas según la función, lo que facilita la evolución y el mantenimiento del código.

¿Está muerto el patrón repositorio?

Este es uno de los temas más debatidos dentro de la comunidad de .NET Core. Microsoft ha creado EF Core utilizando el patrón de Repositorio y patrón de Unit Of Work. 

Entonces, ¿por qué necesitamos agregar otra capa de abstracción sobre EF Core, que es otra abstracción más de acceso a datos? Microsoft recomiendan usar patrones de repositorio en escenarios complejos para reducir el acoplamiento y proporcionar una mejor capacidad de prueba de sus soluciones. En los casos en los que desee el código más simple posible, querrá evitar el patrón de repositorio.

Una desventaja más de usar directamente el «DbContext» directamente es que estaría exponiéndose y esto es totalmente inseguro.

¿Qué es Unit of Work?

Si el patrón repositorio es nuestra abstracción sobre la idea de almacenamiento persistente, el patrón UoW es nuestra abstracción sobre la idea de operaciones atómicas. Se hace referencia a UoW como una sola transacción que implica múltiples operaciones de inserción, actualización, eliminación, etc.

El patrón de UoW ahora administra los estados de la base de datos. Una vez que se completan todas las actualizaciones de las entidades en un ámbito, los cambios rastreados se reproducen en la base de datos en una transacción para que la base de datos refleje los cambios deseados.

Las transacciones permiten procesar varias operaciones de base de datos de manera atómica. Si se confirma la transacción, todas las operaciones se aplican con éxito a la base de datos. Si se revierte la transacción, ninguna de las operaciones se aplica a la base de datos.

Transacciones con Entity Framework Core

De manera predeterminada, si el proveedor de base de datos admite las transacciones, todos los cambios de una llamada a SaveChanges se aplican a una transacción. Si cualquiera de los cambios presenta un error, la transacción se revertirá y no se aplicará ninguno de los cambios a la base de datos. Esto significa que se garantiza que SaveChanges se complete correctamente o deje sin modificaciones la base de datos si se produce un error.

using var context = new ApplicationContext();

// NEW TRANSACTION
var post = context.Posts.Add(new Post() { Title = "My post" });

context.Users.Add(new User()
{
    FirstName = "Alberto",
    PostId = post.Id
});

context.SaveChanges();
// COMMIT

// NEW TRANSACTION
context.Comments.Add(new Comment() { text = "My comment" });
context.SaveChanges();
// COMMIT

También EF Core nos permite múltiples SaveChanges en una sola transacción, es decir, crear o usar una sola transacción con múltiples SaveChanges():

  • DbContext.Database.BeginTransaction(), crea una nueva transacción para la base de datos subyacente y nos permite confirmar o revertir los cambios realizados en la base de datos mediante varias llamadas al método SaveChanges. Si lanzamos una excepción después de la primera llamada a SaveChanges(). Esto ejecutará un bloque catch donde llamamos al método RollBack() para revertir cualquier cambio que se haya realizado en la base de datos.
using var context = new ApplicationContext();

try
{
    // NEW TRANSACTION
    var post = context.Posts.Add(new Post() { Title = "My post" });

    context.Users.Add(new User()
    {
        FirstName = "Alberto",
        PostId = post.Id
    });

    context.SaveChanges();

    // throw exception to test roll back transaction
    throw new Exception();

    context.Comments.Add(new Comment() { text = "My comment" });

    context.SaveChanges();

    transaction.Commit();
    // COMMIT
}
catch (Exception ex)
{
    transaction.Rollback();
    Console.WriteLine("Error occurred.");
}

Hablaremos más en profundidad sobre las transacciones en otro post, aquí nos centraremos en los patrones repositorio y UoW.

Veamos el código👇

En nuestro proyecto implementaremos básicamente 2 interfaces (IRepository y IReadRepository) para nuestros repositorios:

  • Una interfaz llamada IRepository, en la que vamos a definir todas las operaciones de actualización y consultas de datos:
public interface IRepository<TEntity> : IRepositoryBase<TEntity> where TEntity : class { }
public interface IRepositoryBase<TEntity> : IReadRepositoryBase<TEntity> where TEntity : class
{
    IUnitOfWork UnitOfWork { get; }

    TEntity Add(TEntity entity);

    Task<TEntity> AddAsync(TEntity entity, CancellationToken cancellationToken = default);

    ICollection<TEntity> AddRange(ICollection<TEntity> entities);

    Task<int> AddRangeAsync(ICollection<TEntity> entities, CancellationToken cancellationToken = default);

    void Delete(TEntity entity);

    Task<int> DeleteAsync(TEntity entity, CancellationToken cancellationToken = default);

    void DeleteRange(ICollection<TEntity> entities);

    Task<int> DeleteRangeAsync(ICollection<TEntity> entities, CancellationToken cancellationToken = default);

    void Update(TEntity entity);

    Task<int> UpdateAsync(TEntity entity, CancellationToken cancellationToken = default);
}
public interface IReadRepositoryBase<TEntity> where TEntity : class
{
    IQueryable<TEntity> GetAll(bool asNoTracking = true);

    IQueryable<TEntity> GetAllBySpec(Expression<Func<TEntity, bool>> predicate, bool asNoTracking = true);

    Task<TEntity?> GetByIdAsync<TId>(TId id, CancellationToken cancellationToken = default) where TId : notnull;

    Task<TEntity?> GetBySpecAsync<Spec>(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);

    Task<ICollection<TEntity>> ListAsync(CancellationToken cancellationToken = default);

    Task<ICollection<TEntity>> ListAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);

    Task<int> CountAsync(CancellationToken cancellationToken = default);

    Task<int> CountAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);

    Task<bool> AnyAsync(CancellationToken cancellationToken = default);

    Task<bool> AnyAsync(Expression<Func<TEntity, bool>> predicate, CancellationToken cancellationToken = default);

    IQueryable<TEntity> GetAllIncluding(params Expression<Func<TEntity, object>>[] includeProperties);
}
  • En segundo lugar, crearemos una nueva interfaz IReadRepository solamente con operaciones de consultas de la base de datos:
public interface IReadRepository<TEntity> : IReadRepositoryBase<TEntity> where TEntity : class { }
  • Por último, tendremos una interfaz IUnitOfWork para las operaciones atómicas:
public interface IUnitOfWork : IDisposable
{
    Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken));

    bool HasActiveTransaction { get; }

    IDbContextTransaction GetCurrentTransaction();

    Task<IDbContextTransaction> BeginTransactionAsync();

    Task CommitAsync(IDbContextTransaction transaction);
}

Resumiendo, usamos repositorios genéricos para nuestro proyecto y podrán ser usados por cualquier entidad o agregado. Si necesitamos crear un repositorio con operaciones personalizadas, crearíamos un repositorio que herede de «IRepository» o «IReadRepository» e implementaríamos nuestras operaciones personalizadas. Veamos un ejemplo:

public interface ICustomPostRepository : IRepository<Post>, IReadRepository<Post>
{
    new Post Add(Post entity);

    Task<Post?> GetPostWithCommentsAsync(int id);

    IEnumerable<Post> GetPostWithMoreComments(int count);
}

Por ejemplo, una opción es heredad solamente de IReadRepository y de esta manera restringimos que los usuarios del repositorio puedan actualizar datos, solamente podrían consultar:

public interface IPostRepository : IReadRepository<Post>
{
    ...
}

Todas las implementaciones de estas interfaces están disponibles en el código de GitHub, que podéis descargar. También encontraréis ejemplos de uso de estos repositorios.

Eso es todo por el momento, seguiré actualizando y agregando contenido en este post. Espero que te haya resultado interesante😉

Descargas

El código fuente de este artículo se puede encontrar en GitHub