Patrón Unit of Work en C#: Un Enfoque Práctico

Hoy vamos a continuar con lo que podría entenderse como la segunda parte del post sobre el patrón repositorio. Hoy quiero compartir con vosotros un patrón de diseño importante que a veces pasa desapercibido, pero es fundamental en muchas aplicaciones empresariales y web: el Patrón de Unidad de Trabajo o "Unit of Work".

 

 

1 - ¿Qué es el Patrón Unit of Work?

El Patrón Unit of Work es un patrón de diseño que se utiliza para agrupar varias operaciones (usualmente de una base de datos) en una sola "unidad de trabajo", asegurando que todas estas operaciones se ejecuten o no se ejecuten

Suena muy parecido a las transacciones, ¿verdad? En realidad, es una capa de abstracción por encima de las transacciones de base de datos.

 

Nota: como en el caso del patrón repositorio donde el DbSet es su equivalente, DbConext es el patrón unit of work. Lo que pasa es que tanto yo personalmente como una inmensa cantidad de developers, consideramos que DbSet y DbContext son directamente la base datos, así que nos gusta tener una abstracción justo por encima.

 

 

2 - ¿Por qué deberíamos utilizar el patrón Unit Of Work?

Lo mas fácil es que veamos un ejemplo; Digamos que tienes un comercio electrónico. Un usuario hace un pedido, el cual involucra varios pasos como verificar el inventario, crear la orden, descontar el inventario y finalmente, enviar una confirmación por correo electrónico. Cada uno de estos pasos modifica el estado de tu aplicación. (en un monolito, en arquitecturas distribuídas es diferente).

 

Si algo sale mal en medio del proceso, necesitas asegurarte de que la base de datos no quede en un estado inconsistente. Aquí es donde el Patrón Unit of Work es útil. Garantiza que todas las operaciones se realicen con éxito, o si hay una que falla, entonces todas las operaciones se deshacen (roll back).

 

Esta lógica se puede aplicar a cualquier otra aplicación. 

En resumen, lo importante es que apliquemos Unit Of Work para todas aquellas operaciones que necesitan alterar más de una tabla en la base de datos, aunque también podemos tener escenarios donde llamamos a APIs de terceros, pero en el 99% de los casos, va a ser exclusivo de la base de datos.

 

 

3 - Implementando el Patrón Unit of Work en C#

Antes de comenzar, no olvides que este código es parte de un curso, el cual tiene todo el código disponible en GitHub

 

Para implementar el patrón unit of work en C#, es recomendable hacerlo teniendo implementado, o implementando patrón repositorio. así es como lo estamos implementando en este curso. 

 

Lo primero es ponernos en contexto, en el post anterior convertimos la parte del usuario en el patron repositorio, en este caso, vamos a convertir la WorkingExperience

public interface IWorkingExperienceRepository
{
    Task Insert(List<Workingexperience> workingExperiences);
}

public class WorkingExperienceRepository : IWorkingExperienceRepository
{
    private readonly CursoEfContext _context;

    public WorkingExperienceRepository(CursoEfContext cursoEfContext)
    {
        _context = cursoEfContext;
    }

    public async Task Insert(List<Workingexperience> workingExperiences)
        => await _context.Wokringexperiences.AddRangeAsync(workingExperiences);
}

Como vemos, no estamos ejecutando _context.SaveChanges() esto es porque ejecutaremos dicho guardado, cuando vayamos a guardar la unidad de trabajo (unit of work).

 

Ahora lo que debemos hacer es la propia implementación del Unit Of Work. Para ello simplemente creamos una interfaz llamada IUnitOfWork y su clase correspondiente, la cual debe inyectar nuestro DbContext y tener el método para guardar los cambios.

public interface IUnitOfWork : IDisposable
{
    Task<int> Save();
}

public class UnitOfWork : IUnitOfWork
{
    private readonly CursoEfContext _context;

    public UnitOfWork(CursoEfContext context)
    {
        _context = context;
    }

    public async Task<int> Save()
        => await _context.SaveChangesAsync();

    public void Dispose()
    {
        _context.Dispose();
    }
}

Por ahora este código no hace nada, de hecho, lo que haces es implementar IDisposable, y el motivo es porque DbContext implementa IDisposable, así que nosotros debemos implementarlo también. Aquí tienes un post donde explico IDisposable en detalle.  

 

Lo que vamos a hacer ahora es, incluir tanto en la interfaz como en la clase, los repositorios que hemos creado con repository pattern.

public interface IUnitOfWork : IDisposable
{
    IUserRepository UserRepository { get; }
    IWorkingExperienceRepository WorkingExperienceRepository { get; }
    Task<int> Save();
}

public class UnitOfWork : IUnitOfWork
{
    public IUserRepository UserRepository { get; }
    public IWorkingExperienceRepository WorkingExperienceRepository { get; }
    private readonly CursoEfContext _context;

    public UnitOfWork(CursoEfContext context, IUserRepository userRepository, 
        IWorkingExperienceRepository workingExperienceRepository)
    {
        _context = context;
        UserRepository = userRepository;
        WorkingExperienceRepository = workingExperienceRepository;
    }

    public async Task<int> Save()
        => await _context.SaveChangesAsync();

    public void Dispose()
    {
        _context.Dispose();
    }
}
  • Nota: Si seguiste el post anterior, recordarás que guardabamos la información dentro del repositorio. Asegúrate de que eliminas la instrucción savechanges

 

Un punto intermedio, pero que no se nos olvide, es añadir todo lo que hemos creado al contenedor de dependencias.

builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddScoped<IWorkingExperienceRepository, WorkingExperienceRepository>();
builder.Services.AddScoped<IUnitOfWork, UnitOfWork>();

 

Ahora lo que tenemos que hacer es, refactorizar nuestra implementación anterior del código para implementar el patrón unit of work.

Por detrás va a funcionar igual, pero su implementación va a ser mucho más sencilla de entender, mantener y testear.

//Código original
public class RelationsController : Controller
{
    private readonly CursoEfContext _context;


    public RelationsController(CursoEfContext context)
    {
        _context = context;
    }

    [HttpPost("InsertDataExample1")]
    public async Task InsertDataExample1()
    {
        User user1 = new User()
        {
            Email = $"{Guid.NewGuid()}@mail.com",
            UserName = "id1"
        };

        List<Workingexperience> workingExperiences1 = new List<Workingexperience>()
        {
            new Workingexperience()
            {
                UserId = user1.Id,
                Name = "experience 1",
                Details = "details1",
                Environment = "environment"
            },
            new Workingexperience()
            {
                UserId = user1.Id,
                Name = "experience 2",
                Details = "details2",
                Environment = "environment"
            }
        };

        await _context.Users.AddAsync(user1);
        await _context.Wokringexperiences.AddRangeAsync(workingExperiences1);
        await _context.SaveChangesAsync();
    }
}

Lo que voy a hacer también, para simplificar, es mover la lógica a un servicio, a lo que sería la capa de lógica para así no tenerla en el controlador. Esta acción es completamente opcional, pero mantiene el código más limpio.

  • Nota: Inyecta el servicio en el contenedor de dependencias.

 

Antes de continuar tienes que tener en cuenta una cosa, hasta que no insertas el primer valor en la base de datos (User en nuestro caso), no tienes acceso al Id para hacer la referencia (en WorkingExperience), por lo que lo más común que se suele hacer es, crear una propiedad virtual en tu entidad que se llama igual que la tabla donde vas a insertar, por ejemplo, en este ejemplo, WorkingExperiences tiene el campo userId, lo que hacemos es crear un virtual User, que hace referencia a la entidad de la tabla.

public class Workingexperience
{
    public int Id { get; set; }

    public int UserId { get; set; }
    public virtual User User { get; set; }

    [MaxLength(50)]
    public string Name { get; set; }
    
    public string Details { get; set; }

    public string Environment { get; set; }

    public DateTime? StartDate { get; set; }

    public DateTime? EndDate { get; set; }
}

Como digo, esto es una práctica común y el los diferentes ORM saben detectar que el id de User hace referencia a tu UserId.

  • Nota: En Dapper por ejemplo, debes hacer BeginTransaction y a partir de ahí te da el Id, etc, es un poquito más complejo. 

Pero a su vez, esta accion, puede causarnos problemas si en la api estas devolviendo la entidad, que no deberías (Diferencia entre DTO y Entidad), ya que causa un problema de referencias circulares a nivel del serializer que estes utilizando.

Para evitar esto, debes incluir configuración para el Serializer: 

builder.Services.AddControllers().AddJsonOptions(options =>
{
    options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
});

 

Ahora si que si, todo anda funcionando, modificamos el servicio utilizando IUnitOfWork y haciendo referencia al usuario:

public class InsertUser
{
    private readonly IUnitOfWork _unitOfWork;

    public InsertUser(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }

    public async Task<bool> Execute(int id)
    {
        User user = new User()
        {
            Email = $"{Guid.NewGuid()}@mail.com",
            UserName = $"id{id}"
        };
       
        List<Workingexperience> workingExperiences = new List<Workingexperience>()
        {
            new Workingexperience()
            {
                User = user,
                Name = $"experience1 user {id}",
                Details = "details1",
                Environment = "environment"
            },
            new Workingexperience()
            {
                User = user,
                Name = $"experience user {id}",
                Details = "details2",
                Environment = "environment"
            }
        };
        
        _ = await _unitOfWork.UserRepository.Insert(user);
        await _unitOfWork.WorkingExperienceRepository.Insert(workingExperiences);
        _ = await _unitOfWork.Save();
        
        return true;
    }
}

Y vemos que funciona correctamente.

unit of work working example

 

Con esto lo que hemos conseguido es implementar transacciones utilizando el patrón repositorio.

 


Uso del bloqueador de anuncios adblock

Hola!

Primero de todo bienvenido a la web de NetMentor donde podrás aprender programación en C# y .NET desde un nivel de principiante hasta más avanzado.


Yo entiendo que utilices un bloqueador de anuncios como AdBlock, Ublock o el propio navegador Brave. Pero te tengo que pedir por favor que desactives el bloqueador para esta web.


Intento personalmente no poner mucha publicidad, la justa para pagar el servidor y por supuesto que no sea intrusiva; Si pese a ello piensas que es intrusiva siempre me puedes escribir por privado o por Twitter a @NetMentorTW.


Si ya lo has desactivado, por favor recarga la página.


Un saludo y muchas gracias por tu colaboración

© copyright 2024 NetMentor | Todos los derechos reservados | RSS Feed

Buy me a coffee Invitame a un café