Entity Framework 核心 1.0 工作单元与 Asp.Net 核心中间件或 Mvc 过滤器

Entity Framework Core 1.0 unit of work with Asp.Net Core middleware or Mvc filter

我正在使用 EF Core 1.0(以前称为 EF7)和 ASP.NET Core 1.0(以前称为 ASP.NET 5)RESTful API。

我希望将一些工作单元限定在一个 http 请求范围内,这样在响应 HTTP 请求时要么将对 DbContext 所做的所有更改保存到数据库中,要么 none 将被保存(例如,如果有一些异常)。

过去,我通过使用动作过滤器将 WebAPI2 与 NHibernate 一起用于此目的,在动作过滤器中,我在执行动作时开始事务,在执行动作时结束事务并关闭会话。这是 http://isbn.directory/book/9781484201107

推荐的方式

但是现在我正在使用 Asp.Net Core(使用 Asp.Net Core Mvc,尽管这应该不相关)和 Entity Framework,据我所知,它已经实现了一个工作单元。

我认为将中间件插入 ASP.NET 管道(在 MVC 之前)将是正确的做事方式。所以一个请求会去:

PIPELINE ASP.NET: MyUnitOfWorkMiddleware ==> MVC 控制器 ==> 存储库 ==> MVC 控制器 ==> MyUnitOfWorkMiddleware

我正在考虑让这个中间件在没有异常发生的情况下保存 DbContext 更改,这样在我的存储库实现中我什至不需要做 dbcontext.SaveChanges(),一切都会像一个集中式事务。在伪代码中我猜它会是这样的:

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
     }
}

这有意义吗?有人有类似的例子吗?我找不到任何好的做法或建议。

此外,如果我在我的 MVC 控制器级别采用这种方法,我将无法访问数据库在 POST 创建新资源时创建的任何资源 ID,因为直到dbContext 更改被保存(稍后在控制器完成执行后在我的中间件的管道中)。如果我需要在我的控制器中访问新创建的资源 ID 怎么办?

如有任何建议,我们将不胜感激!

更新 1我发现我使用中间件实现此目的的方法存在问题,因为中间件中的 DbContext 实例与MVC(和存储库)生命周期。看问题

更新 2:我还没有找到好的解决方案。到目前为止,基本上这些是我的选择:

  1. 尽快保存数据库中的更改。这意味着将其保存在存储库实现本身上。这种方法的问题是,对于一个 Http 请求,我可能想使用多个存储库(即:在数据库中保存一些东西,然后将一个 blob 上传到云存储)并且为了拥有一个工作单元,我必须实现一个处理多个实体甚至多个持久化方法(DB 和 Blob 存储)的存储库,这违背了整个目的
  2. 实施 Action Filter,我将整个操作执行包装在数据库事务中。在控制器的动作执行结束时,如果没有异常,我将更改提交给 DB,但如果有异常,我回滚并丢弃上下文。这个问题是我的控制器的操作可能需要一个生成的实体 ID,以便 return 它到 http 客户端(即:如果我得到一个 POST /api/cars 我想 return a 201 Accepted with a location header 标识在 /api/cars/123 创建的新资源并且 ID 123 尚不可用,因为该实体尚未保存在数据库中并且 ID 仍然是临时的0). POST 动词请求的控制器操作示例:

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

我怎样才能将整个控制器的操作包装在数据库事务中,同时让数据库生成的任何 Id 可用,以便 return 它来自控制器的 Http 响应?或者.. 是否有任何优雅的方法来覆盖 http 响应并在提交数据库更改后在操作过滤器级别设置 Id?

更新 3: 根据 nathanaldensr 的评论,我可以两全其美(将我的控制器的动作执行包装在数据库事务中 _ UoW 以及甚至在数据库提交更改之前就知道创建的新资源的 Id)通过使用代码生成的 Guid 而不是依赖数据库来生成 Guid。

我的建议是,在控制器中使用 dbContext.SaveChanges(),因为它在网络上的所有示例中都有展示。你想做的事情听起来很花哨,可能会适得其反,正如你在 post 结束时所猜测的那样。而 IMO,这没有意义。

关于你的第二个 question/task:

....when responding to the HTTP request either ALL the changes made to the DbContext will be saved onto the database, or none will be saved (if there was some exception, for example).

我想你需要像 'transaction-per-request' 这样的东西。这只是一个想法,根本没有测试过。我只是将代码放在这个示例中间件中:

public class TransactionPerRequestMiddleware
{
    private readonly RequestDelegate next_;

    public TransactionPerRequestMiddleware(RequestDelegate next)
    {
        next_ = next;
    }

    public async Task Invoke(HttpContext context, DbContext dbContext)
    {
        var transaction = dbContext.Database.BeginTransaction(
            System.Data.IsolationLevel.ReadCommitted);

        await next_.Invoke(context);

        if (context.Response.StatusCode == 200)
        {
            transaction.Commit();
        }
        else
        {
            transaction.Rollback();
        }
    }
}

祝你好运

根据Entity Framework Core 1.0 DbContext not scoped to http request 我无法使用中间件来实现此目的,因为中间件注入的 DbContext 实例与 MVC 执行期间的 DbContext 不同(在我的控制器或存储库中)。

在使用全局过滤器执行控制器的操作后,我不得不采用类似的方法来保存 DbContext 中的更改。 目前还没有关于 MVC 6 中的过滤器的官方文档,所以如果有人对此解决方案感兴趣,请参阅下面的过滤器以及我将此过滤器设置为全局的方式,以便它在任何控制器的操作之前执行。

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");
        }
    }
}

并且在配置 MVC 时,过滤器在 Startup.cs 插入我的 MVC。

public void ConfigureServices(IServiceCollection services)
{
   //..
   //Entity Framework 7
   services.AddEntityFramework()
            .AddSqlServer()
            .AddDbContext<SpeediCargoDbContext>(options => {
               options.UseSqlServer(Configuration["Data:DefaultConnection:ConnectionString"]);
            });

        //MVC 6
        services.AddMvc(setup =>
        {
            setup.Filters.AddService(typeof(UnitOfWorkFilter));
        });
   //..
}

这还有一个问题(请参阅我的问题的更新 2)。 如果我希望我的控制器响应 201 接受的 http POST 请求,其位置 header 包含在数据库中创建的实体的 ID,该怎么办?当控制器的操作完成执行时,更改尚未提交到数据库,因此创建的实体的 Id 仍然为 0,直到操作过滤器保存更改并且数据库生成一个值。

我也遇到了同样的问题,不知道该采用哪种方法。 我使用的方法之一如下:

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();
    }
}