Unidad de trabajo Entity Framework Core 1.0 con Asp.Net Core middleware o filtro Mvc

asp.net-core-1.0 entity-framework-core middleware unit-of-work

Pregunta

Estoy utilizando EF Core 1.0 (anteriormente conocido como EF7) y ASP.NET Core 1.0 (anteriormente conocido como ASP.NET 5) para una API RESTful.

Me gustaría tener alguna unidad de trabajo en el ámbito de una solicitud http de tal manera que al responder a la solicitud HTTP, TODOS los cambios realizados en el DbContext se guardarán en la base de datos, o ninguno se guardará (si hubiera) alguna excepción, por ejemplo).

En el pasado, he usado WebAPI2 para este propósito con NHibernate utilizando un filtro de Acción donde comienzo la transacción en la ejecución de la acción, y en la acción ejecutada, finalizo la transacción y cierro la sesión. Esta fue la forma recomendada en http://isbn.directory/book/9781484201107

Sin embargo, ahora estoy usando Asp.Net Core (con Asp.Net Core Mvc, aunque esto no debería ser relevante) y Entity Framework que, según tengo entendido, ya implementa una unidad de trabajo.

Creo que tener un middleware conectado a la tubería de ASP.NET (antes de MVC) sería la manera correcta de hacer las cosas. Así que una solicitud iría:

PIPELINE ASP.NET: MyUnitOfWorkMiddleware ==> MVC Controller ==> Repository ==> MVC Controller ==> MyUnitOfWorkMiddleware

Estaba pensando en hacer que este middleware guarde los cambios de DbContext si no ocurriera una excepción, de modo que en las implementaciones de mi repositorio ni siquiera necesito hacer dbcontext.SaveChanges() y todo sería como una transacción centralizada. En pseudocódigo supongo que sería algo como:

class MyUnitOfWorkMiddleware
{
     //..
     1-get an instance of DbContext for this request.
     try {
         2-await the next item in the pipeline.
         3-dbContext.SaveChanges();
     }
     catch (Exception e) {
         2.1-rollback changes (simply by ignoring context)
         2.2-return an http error response
     }
}

¿Esto tiene sentido? ¿Alguien tiene algún ejemplo de algo similar? No puedo encontrar ninguna buena práctica o recomendación sobre esto.

Además, si utilizo este enfoque en mi nivel de controlador MVC, no tendré acceso a ningún ID de recurso creado por la base de datos al enviar un nuevo recurso porque la ID no se generará hasta que se guarden los cambios de dbContext (más adelante en la canalización en mi middleware DESPUÉS de que el controlador haya terminado de ejecutarse). ¿Qué sucede si necesito acceder a la ID recién creada de un recurso en mi controlador?

Cualquier consejo sería muy apreciado!

ACTUALIZACIÓN 1 : Encontré un problema con mi enfoque para usar middleware para lograr esto porque la instancia de DbContext en el middleware no es la misma que durante la vida útil de MVC (y repositorios). Consulte la pregunta Entity Framework Core 1.0 DbContext no incluido en la solicitud http

ACTUALIZACIÓN 2 : Todavía no he encontrado una buena solución. Básicamente estas son mis opciones hasta ahora:

  1. Guarde los cambios en DB tan pronto como sea posible. Eso significa guardarlo en la propia implementación del repositorio. El problema con este enfoque es que para una solicitud Http tal vez quiera usar varios repositorios (es decir, guardar algo en la base de datos y luego cargar un blob en un almacenamiento en la nube) y para tener una Unidad de Trabajo tendría que implementar un repositorio que se ocupa de más de una entidad o incluso más de un método de persistencia (DB y Blob Storage), que anula todo el propósito
  2. Implementar un filtro de acción donde envuelvo toda la ejecución de la acción en una transacción de base de datos. Al final de la ejecución de la acción del controlador, si no hay excepciones, confirmo cambios en la base de datos, pero si hay excepciones, retrocedo y descarto el contexto. El problema con esto es que la acción de mi controlador puede necesitar la identificación de una entidad generada para devolverla al cliente http (es decir: si obtengo un POST / api / cars, deseo devolver un 201 Aceptado con un encabezado de ubicación que identifique el nuevo recurso creado en / api / cars / 123 y el Id 123 no estarían disponibles aún, ya que la entidad no se ha guardado en la base de datos y el Id sigue siendo un 0 temporal. Ejemplo en la acción del controlador para una solicitud de verbo POST:

    return CreatedAtRoute("GetCarById", new { carId= carSummaryCreated.Id }, carSummaryCreated); //carSummaryCreated.Id would be 0 until the changes are saved in DB

¿Cómo podría tener toda la acción del controlador envuelta en una transacción de base de datos y, al mismo tiempo, tener disponible cualquier ID generada por la base de datos para devolverla en la Respuesta Http del controlador? O ... ¿existe alguna manera elegante de sobrescribir la respuesta http y establecer la Id. En el nivel del filtro de acción una vez que se hayan comprometido los cambios en la base de datos?

ACTUALIZACIÓN 3: De acuerdo con el comentario de nathanaldensr , pude obtener lo mejor de ambos mundos (envolviendo la ejecución de la acción de mi controlador en una transacción de base de datos _ UoW y también conociendo la identificación del nuevo recurso creado incluso antes de que la base de datos confirme cambios) utilizando el código generado Guids en lugar de confiar en la base de datos para generar el Guid.

Respuesta aceptada

También estoy enfrentando el mismo problema y no estoy seguro de qué enfoque seguir. Uno de los enfoques que utilicé es el siguiente:

public class UnitOfWorkFilter : ActionFilterAttribute
{
    private readonly AppDbContext _dbContext;

    public UnitOfWorkFilter(AppDbContext dbContext,)
    {
        _dbContext = dbContext;
    }

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        if (!context.HttpContext.Request.Method.Equals("Post", StringComparison.OrdinalIgnoreCase))
            return;
        if (context.Exception == null && context.ModelState.IsValid)
        {
            _dbContext.Database.CommitTransaction();
        }
        else
        {
            _dbContext.Database.RollbackTransaction();
        }
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        if (!context.HttpContext.Request.Method.Equals("Post", StringComparison.OrdinalIgnoreCase))
            return;
        _dbContext.Database.BeginTransaction();
    }
}

Respuesta popular

Según Entity Framework Core 1.0 DbContext no se aplicó a la solicitud http no pude usar un middleware para lograrlo porque la instancia de DbContext que se inyecta no es la misma que DbContext durante la ejecución de MVC (en mis controladores o repositorios).

Tuve que seguir un enfoque similar para guardar los cambios en DbContext después de la ejecución de la acción del controlador mediante un filtro global. Aún no hay documentación oficial sobre los filtros en MVC 6, por lo que si alguien está interesado en esta solución, vea a continuación el filtro y la forma en que hago este filtro global para que se ejecute antes de la acción de cualquier controlador.

public class UnitOfWorkFilter : ActionFilterAttribute
{
    private readonly MyDbContext _dbContext;
    private readonly ILogger _logger;

    public UnitOfWorkFilter(MyDbContext dbContext, ILoggerFactory loggerFactory)
    {
        _dbContext = dbContext;
        _logger = loggerFactory.CreateLogger<UnitOfWorkFilter>();
    }

    public override async Task OnActionExecutionAsync(ActionExecutingContext executingContext, ActionExecutionDelegate next)
    {
        var executedContext = await next.Invoke(); //to wait until the controller's action finalizes in case there was an error
        if (executedContext.Exception == null)
        {
            _logger.LogInformation("Saving changes for unit of work");
            await _dbContext.SaveChangesAsync();
        }
        else
        {
            _logger.LogInformation("Avoid to save changes for unit of work due an exception");
        }
    }
}

y el filtro se conecta a mi MVC en Startup.cs al configurar MVC.

public class UnitOfWorkFilter : ActionFilterAttribute
{
    private readonly MyDbContext _dbContext;
    private readonly ILogger _logger;

    public UnitOfWorkFilter(MyDbContext dbContext, ILoggerFactory loggerFactory)
    {
        _dbContext = dbContext;
        _logger = loggerFactory.CreateLogger<UnitOfWorkFilter>();
    }

    public override async Task OnActionExecutionAsync(ActionExecutingContext executingContext, ActionExecutionDelegate next)
    {
        var executedContext = await next.Invoke(); //to wait until the controller's action finalizes in case there was an error
        if (executedContext.Exception == null)
        {
            _logger.LogInformation("Saving changes for unit of work");
            await _dbContext.SaveChangesAsync();
        }
        else
        {
            _logger.LogInformation("Avoid to save changes for unit of work due an exception");
        }
    }
}

Esto todavía deja una pregunta (ver ACTUALIZACIÓN 2 en mi pregunta). ¿Qué sucede si deseo que mi controlador responda a una solicitud HTTP POST con un 201 Aceptado con un encabezado de ubicación que incluya la identificación de la entidad creada en la base de datos? Cuando la acción del controlador finaliza la ejecución, los cambios aún no se han confirmado en la base de datos, por lo que el Id. De la entidad creada sigue siendo 0 hasta que el filtro de acción guarda los cambios y la base de datos genera un valor.




Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué
Licencia bajo: CC-BY-SA with attribution
No afiliado con Stack Overflow
¿Es esto KB legal? Sí, aprende por qué