Interceptor UnitOfWork de ABP - UnitOfWorkRegistrar (Segunda Parte)

En el artículo anterior exploramos cómo ABP utiliza UnitOfWork y todo el proceso de ejecución en el sistema. Ahora analizaremos en detalle qué sucede internamente cuando se ejecutan los métodos Begin y Complete del UnitOfWorkManager. Examinemos la clase UnitOfWorkManager, aunque si no has leído la parte anterior, te recomiendo hacerlo primero.

/// <summary>
    /// Gestor de unidad de trabajo.
    /// </summary>
    internal class GestorUnidadTrabajo : IGestorUnidadTrabajo, IDependenciaTransitoria
    {
        private readonly IResolvedorIoc _resolvedorIoc;
        private readonly IProveedorUowActual _proveedorUowActual;
        private readonly IOpcionesDefaultUow _opcionesDefault;

        public IUnidadTrabajoActiva Actual
        {
            get { return _proveedorUowActual.Actual; }
        }

        public GestorUnidadTrabajo(
            IResolvedorIoc resolvedorIoc,
            IProveedorUowActual proveedorUowActual,
            IOpcionesDefaultUow opcionesDefault)
        {
            _resolvedorIoc = resolvedorIoc;
            _proveedorUowActual = proveedorUowActual;
            _opcionesDefault = opcionesDefault;
        }

        public IHandleCompletadoUow Iniciar()
        {
            return Iniciar(new OpcionesUnidadTrabajo());
        }

        public IHandleCompletadoUow Iniciar(OptionAlcanceTransaccion alcance)
        {
            return Iniciar(new OpcionesUnidadTrabajo { Alcance = alcance });
        }

        public IHandleCompletadoUow Iniciar(OpcionesUnidadTrabajo opciones)
        {
            opciones.RellenarDefaultsParaOpcionesNoProporcionadas(_opcionesDefault);

            var uowExterno = _proveedorUowActual.Actual;

            if (opciones.Alcance == OptionAlcanceTransaccion.Requerido && uowExterno != null)
            {
                return new ManejadorUowInterno();
            }

            var uow = _resolvedorIoc.Resolver<IUnidadTrabajo>();

            uow.Completado += (remitente, argumentos) =>
            {
                _proveedorUowActual.Actual = null;
            };

            uow.Fallido += (remitente, argumentos) =>
            {
                _proveedorUowActual.Actual = null;
            };

            uow.Eliminado += (remitente, argumentos) =>
            {
                _resolvedorIoc.Liberar(uow);
            };

            //Heredar filtros de UOW externo
            if (uowExterno != null)
            {
                opciones.RellenarFiltrosUowExterno(uowExterno.Filtros.ToList());
            }

            uow.Iniciar(opciones);

            //Heredar tenant del UOW externo
            if (uowExterno != null)
            {
                uow.EstablecerTenantId(uowExterno.ObtenerTenantId(), false);
            }

            _proveedorUowActual.Actual = uow;

            return uow;
        }
    }

Antes de analizar esta clase, necesitamos comprender su proceso de inicialización. En dicha inicialización, se inyectan tres dependencias críticas: IResolvedorIoc, IProveedorUowActual e IOpcionesDefaultUow. El primer componente, IResolvedorIoc, resulta familiar pois responsable de obtener instancias específicas del contenedor IoC para interfaces determinadas. IOpcionesDefaultUow, analizada previamente, configura atributos fundamentales y otras opciones de construcción para UnitOfWork. ¿Cuál es entonces el propósito de IProveedorUowActual? Examinemos su implementación concreta.

/// <summary>
    /// Implementación CallContext de <see cref="IProveedorUowActual"/>. 
    /// Esta es la implementación predeterminada.
    /// </summary>
    public class ProveedorUowAsyncLocal : IProveedorUowActual, IDependenciaTransitoria
    {
        /// <inheritdoc />
        [NoEnlazar]
        public IUnidadTrabajo Actual
        {
            get { return ObtenerUowActual(); }
            set { EstablecerUowActual(value); }
        }

        public IRegistrador Registro { get; set; }

        private static readonly AsyncLocal<ContenedorUowLocal> UowAsyncLocal = new AsyncLocal<ContenedorUowLocal>();

        public ProveedorUowAsyncLocal()
        {
            Registro = RegistradorNulo.Instancia;
        }

        private static IUnidadTrabajo ObtenerUowActual()
        {
            var uow = UowAsyncLocal.Value?.UnidadTrabajo;
            if (uow == null)
            {
                return null;
            }

            if (uow.EstaEliminado)
            {
                UowAsyncLocal.Value = null;
                return null;
            }

            return uow;
        }

        private static void EstablecerUowActual(IUnidadTrabajo valor)
        {
            lock (UowAsyncLocal)
            {
                if (valor == null)
                {
                    if (UowAsyncLocal.Value == null)
                    {
                        return;
                    }

                    if (UowAsyncLocal.Value.UnidadTrabajo?.Externo == null)
                    {
                        UowAsyncLocal.Value.UnidadTrabajo = null;
                        UowAsyncLocal.Value = null;
                        return;
                    }

                    UowAsyncLocal.Value.UnidadTrabajo = UowAsyncLocal.Value.UnidadTrabajo.Externo;
                }
                else
                {
                    if (UowAsyncLocal.Value?.UnidadTrabajo == null)
                    {
                        if (UowAsyncLocal.Value != null)
                        {
                            UowAsyncLocal.Value.UnidadTrabajo = valor;
                        }

                        UowAsyncLocal.Value = new ContenedorUowLocal(valor);
                        return;
                    }

                    valor.Externo = UowAsyncLocal.Value.UnidadTrabajo;
                    UowAsyncLocal.Value.UnidadTrabajo = valor;
                }
            }
        }

        private class ContenedorUowLocal
        {
            public IUnidadTrabajo UnidadTrabajo { get; set; }

            public ContenedorUowLocal(IUnidadTrabajo unidadTrabajo)
            {
                UnidadTrabajo = unidadTrabajo;
            }
        }
    }

El elemento central de esta clase lo conforman los métodos ObtenerUowActual() y EstablecerUowActual(IUnidadTrabajo valor). A primera vista, su funcionamiento no resulta evidente. Consultando la documentación oficial de ABP, encontramos la siguiente descripción:

Una Unidad de Trabajo Llama a Otra

La unidad de trabajo es ambiental. Si un método con unidad de trabajo invoca a otro método con unidad de trabajo, ambos comparten la misma conexión y transacción. El primer método gestiona la conexión y los demás métodos la reutilizan.

Esto implica que cuando un método con unit of work llama a otro método similar, ambos comparten conexión y transacción, correspondiendo al primero gestionar dicha unit of work para que el segundo la reuse. El componente IProveedorUowActual resuelve precisamente el problema de múltiples unidades de trabajo anidadas.

Sin embargo, necesitamos comprender mejor esta clase relacionándola con el método Iniciar de GestorUnidadTrabajo.

public IHandleCompletadoUow Iniciar(OpcionesUnidadTrabajo opciones)
        {
            opciones.RellenarDefaultsParaOpcionesNoProporcionadas(_opcionesDefault);

            var uowExterno = _proveedorUowActual.Actual;

            if (opciones.Alcance == OptionAlcanceTransaccion.Requerido && uowExterno != null)
            {
                return new ManejadorUowInterno();
            }

            var uow = _resolvedorIoc.Resolver<IUnidadTrabajo>();

            uow.Completado += (remitente, argumentos) =>
            {
                _proveedorUowActual.Actual = null;
            };

            uow.Fallido += (remitente, argumentos) =>
            {
                _proveedorUowActual.Actual = null;
            };

            uow.Eliminado += (remitente, argumentos) =>
            {
                _resolvedorIoc.Liberar(uow);
            };

            //Heredar filtros de UOW externo
            if (uowExterno != null)
            {
                opciones.RellenarFiltrosUowExterno(uowExterno.Filtros.ToList());
            }

            uow.Iniciar(opciones);

            //Heredar tenant del UOW externo
            if (uowExterno != null)
            {
                uow.EstablecerTenantId(uowExterno.ObtenerTenantId(), false);
            }

            _proveedorUowActual.Actual = uow;

            return uow;
        }

En este método, lo primero que ocurre es obtener _proveedorUowActual.Actual, es decir, la única Unit of Work del método en ejecución. Si la unit of work actual no es null, simplemente retorna un objeto ManejadorUowInterno, el cual contiene operaciones estándar básicas. Veamos el código.

internal class ManejadorUowInterno : IHandleCompletadoUow
    {
        public const string MensajeExcepcionNoLlamoMetodoCompleto = "No se llamó al método Complete de una unidad de trabajo.";

        private volatile bool _completoLlamado;
        private volatile bool _eliminado;

        public void Completar()
        {
            _completoLlamado = true;
        }

        public Task CompletarAsync()
        {
            _completoLlamado = true;
            return Task.FromResult(0);
        }

        public void Eliminar()
        {
            if (_eliminado)
            {
                return;
            }

            _eliminado = true;

            if (!_completoLlamado)
            {
                if (TieneExcepcion())
                {
                    return;
                }

                throw new ExcepcionABP(MensajeExcepcionNoLlamoMetodoCompleto);
            }
        }
        
        private static bool TieneExcepcion()
        {
            try
            {
                return Marshal.GetExceptionCode() != 0;
            }
            catch (Exception)
            {
                return false;
            }
        }
    }

La lógica es clara: cuando una unidad de trabajo interna invoca otra, la segunda, al ejecutar _proveedorUowActual.Actual, obtiene un valor no nulo, por lo que al terminar solo necesita establecer _completoLlamado en true. En cambio, cuando la primera unidad de trabajo ejecuta Iniciar, al llamar a _proveedorUowActual.Actual no puede obtener una instancia específica, entonces mediante var uow = _resolvedorIoc.Resolver<IUnidadTrabajo>() obtiene un objeto, y tras ejecutar _proveedorUowActual.Actual = uow, la primera unidad de trabajo también recibe una instancia única de IUnidadTrabajo, compartida internamente por las unidades de trabajo anidadas. Este diseño garantiza ingeniosamente que el primer método y los métodos internos mantengan una única unidad de trabajo, ejecutando la serie de operaciones subsiguientes únicamente cuendo la primera unidad de trabajo finaliza.

El nucleo de la lógica resides en el objeto que hereda de IUnidadTrabajo, responsable de gestionar conexiones a la base de datos y operaciones transactionales. ABP contiene múltiples implementaciones de IUnidadTrabajo, pero nos centraremos en EntityFrameworkCore, dejando de lado otras como MongoDB. Los métodos definidos en IUnidadTrabajo operan desde UnitOfWorkBase, cuyo contenido es extenso, por lo que mostraremos únicamente las partes esenciales.

 public void Iniciar(OpcionesUnidadTrabajo opciones)
        {
            Verificar.NoNulo(opciones, nameof(opciones));

            PrevenirMultiplesInicios();
            Opciones = opciones;

            EstablecerFiltros(opciones.SobrecargasFiltro);

            EstablecerTenantId(ABpSesion.TenantId, false);

            IniciarUow();
        }

Aquí se realizan principalmente asignaciones de IReadOnlyList<ConfiguracionFiltroDatos> Filtros, se establece el TenantId, y finalmente se ejecuta IniciarUow, un método virtual que las diferentes implementaciones de ORM sobrescriben para completar las operaciones de conexión y transacción a la base de datos. Analicemos qué realiza la clase EfCoreUnidadTrabajo:

/// <summary>
    /// Implementa la unidad de trabajo para Entity Framework.
    /// </summary>
    public class EfCoreUnidadTrabajo : UnidadTrabajoBase, IDependenciaTransitoria
    {
        protected IDictionary<string, DbContext> ContextosActivos { get; }
        protected IResolvedorIoc ResolvedorIoc { get; }

        private readonly IResolvedorDbContext _resolvedorDbContext;
        private readonly IMapeadorTiposDbContext _mapeadorTiposDbContext;
        private readonly IEstrategiaTransaccionEfCore _estrategiaTransaccion;

        /// <summary>
        /// Crea un nuevo <see cref="EfCoreUnidadTrabajo"/>.
        /// </summary>
        public EfCoreUnidadTrabajo(
            IResolvedorIoc resolvedorIoc,
            IResolvedorCadenaConexion resolvedorCadenaConexion,
            IEjecutorFiltroUow ejecutorFiltros,
            IResolvedorDbContext resolvedorDbContext,
            IOpcionesDefaultUow opcionesDefault,
            IMapeadorTiposDbContext mapeadorTiposDbContext,
            IEstrategiaTransaccionEfCore estrategiaTransaccion)
            : base(
                  resolvedorCadenaConexion,
                  opcionesDefault,
                  ejecutorFiltros)
        {
            ResolvedorIoc = resolvedorIoc;
            _resolvedorDbContext = resolvedorDbContext;
            _mapeadorTiposDbContext = mapeadorTiposDbContext;
            _estrategiaTransaccion = estrategiaTransaccion;

            ContextosActivos = new Dictionary<string, DbContext>();
        }

        protected override void IniciarUow()
        {
            if (Opciones.EsTransaccional == true)
            {
                _estrategiaTransaccion.InicializarOpciones(Opciones);
            }
        }

        public override void GuardarCambios()
        {
            foreach (var dbContext in ObtenerTodosLosDbContextsActivos())
            {
                GuardarCambiosEnDbContext(dbContext);
            }
        }

        public override async Task GuardarCambiosAsync()
        {
            foreach (var dbContext in ObtenerTodosLosDbContextsActivos())
            {
                await GuardarCambiosEnDbContextAsync(dbContext);
            }
        }

        protected override void CompletarUow()
        {
            GuardarCambios();
            ConfirmarTransaccion();
        }

        protected override async Task CompletarUowAsync()
        {
            await GuardarCambiosAsync();
            ConfirmarTransaccion();
        }

        private void ConfirmarTransaccion()
        {
            if (Opciones.EsTransaccional == true)
            {
                _estrategiaTransaccion.Confirmar();
            }
        }

        public IReadOnlyList<DbContext> ObtenerTodosLosDbContextsActivos()
        {
            return ContextosActivos.Values.ToImmutableList();
        }

        public virtual TDbContext ObtenerOCrearDbContext<TDbContext>(MultiTenancySides? ladoMultitenancy = null)
            where TDbContext : DbContext
        {
            var tipoDbContextConcreto = _mapeadorTiposDbContext.ObtenerTipoConcreto(typeof(TDbContext));

            var argumentosResolucionCadena = new ArgumentosResolucionCadenaConexion(ladoMultitenancy);
            argumentosResolucionCadena["TipoDbContext"] = typeof(TDbContext);
            argumentosResolucionCadena["TipoDbContextConcreto"] = tipoDbContextConcreto;
            var cadenaConexion = ResolverCadenaConexion(argumentosResolucionCadena);

            var claveDbContext = tipoDbContextConcreto.FullName + "#" + cadenaConexion;

            DbContext dbContext;
            if (!ContextosActivos.TryGetValue(claveDbContext, out dbContext))
            {
                if (Opciones.EsTransaccional == true)
                {
                    dbContext = _estrategiaTransaccion.CrearDbContext<TDbContext>(cadenaConexion, _resolvedorDbContext);
                }
                else
                {
                    dbContext = _resolvedorDbContext.Resolver<TDbContext>(cadenaConexion, null);
                }

                if (Opciones.TiempoLimite.HasValue &&
                    dbContext.Database.EsRelacional() && 
                    !dbContext.Database.ObtenerTiempoLimiteComando().HasValue)
                {
                    dbContext.Database.EstablecerTiempoLimiteComando(Opciones.TiempoLimite.Value.TotalSeconds.To<int>());
                }
                ContextosActivos[claveDbContext] = dbContext;
            }

            return (TDbContext)dbContext;
        }

        protected override void EliminarUow()
        {
            if (Opciones.EsTransaccional == true)
            {
                _estrategiaTransaccion.Eliminar(ResolvedorIoc);
            }
            else
            {
                foreach (var contexto in ObtenerTodosLosDbContextsActivos())
                {
                    Liberar(contexto);
                }
            }

            ContextosActivos.Clear();
        }

        protected virtual void GuardarCambiosEnDbContext(DbContext dbContext)
        {
            dbContext.SaveChanges();
        }

        protected virtual async Task GuardarCambiosEnDbContextAsync(DbContext dbContext)
        {
            await dbContext.SaveChangesAsync();
        }

        protected virtual void Liberar(DbContext dbContext)
        {
            dbContext.Dispose();
            ResolvedorIoc.Liberar(dbContext);
        }        
    }

En esta clase encontramos dos métodos fundamentales: IniciarUow(), invocado al ejecutar el método Iniciar de UnitOfWorkManager, y CompletarUow(), llamado finalmente cuando UnitOfWorkManager ejecuta el método Complete después de completar el método interceptado. Dentro de CompletarUow() se ejecutan secuencialmente GuardarCambios() y ConfirmarTransaccion(). El primero obtiene todos los DbContext activos y ejecuta SaveChanges() en cada uno para persistir los cambios en la base de datos, mientras que el segundo confirma la transacción.

En IniciarUow() verificamos si Opciones.EsTransaccional es true; este valor proviene de UnitOfWorkOptions pasado al ejecutar IUnidadTrabajo.Iniciar para configurar los parámetros de UnitOfWork. Dicha propiedad controla si UnitOfWork ejecuta transacciones, algo documentado en ABP.

Unidad de Trabajo No Transaccional

Por naturaleza, una unidad de trabajo es transaccional. ASP.NET Boilerplate inicia, confirma o revierte una transacción explícita a nivel de base de datos. En casos especiales, la transación puede causar problemas ya que podría bloquear filas o tablas en la base de datos. En estas situaciones, deseas deshabilitar la transacción a nivel de base de datos. El atributo UnitOfWork puede recibir un valor booleano en su constructor para funcionar como no transaccional.

Para deshabilitar transacciones en UnitOfWork dentro de ABP, se puede añadir directamente IsTransactional:false al atributo UnitOfWork personalizado.

[UnidadDeTrabajo(esTransaccional: false)]
public ObtenerTareasSalida ObtenerTareas(ObtenerTareasEntrada entrada)
{
    var tareas = _repositorioTareas.ObtenerTodasConPersonas(entrada.IdPersonaAsignada, estado);
    return new ObtenerTareasSalida
            {
                Tareas = Mapeador.Map<List<Dtos.Tarea>>(tareas)
            };
} 

Publicado el 7-9 08:30