【问题标题】:Entity Framework Core 1.0 unit of work with Asp.Net Core middleware or Mvc filterEntity Framework Core 1.0 工作单元与 Asp.Net Core 中间件或 Mvc 过滤器
【发布时间】:2016-02-09 22:40:29
【问题描述】:

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

我希望将某个工作单元限定为 http 请求,这样在响应 HTTP 请求时,对 DbContext 所做的所有更改都将保存到数据库中,或者不保存(例如,如果有一些例外)。

过去,我通过使用操作过滤器将 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(稍后在在控制器完成执行后,在我的中间件的管道中)。如果我需要访问控制器中新创建的资源 ID 怎么办?

任何建议将不胜感激!

更新 1我发现使用中间件实现此目的的方法存在问题,因为中间件中的 DbContext 实例与 MVC(和存储库)生命周期中的实例不同。见问题Entity Framework Core 1.0 DbContext not scoped to http request

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

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

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

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

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

【问题讨论】:

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


    【解决方案1】:

    根据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 Accepted 且 Location 标头包含在 DB 中创建的实体的 ID 的 http POST 请求怎么办? 当控制器的操作完成执行时,更改尚未已提交给 DB,因此创建的实体的 Id 仍然为 0,直到操作过滤器保存更改并且 DB 生成一个值。

    【讨论】:

      【解决方案2】:

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

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

      【讨论】:

      • 我喜欢你可以用工作单元过滤器装饰任何动作或控制器,以便在 mvc 生命周期中将所有内容包装在事务中。
      • 问题仍然是当您需要实体 ID 以在同一个 http 请求中继续执行操作时该怎么做。在数据库中提交更改之前,不会生成 Id。使用这种方法,在控制器完成执行之前不会保存更改。
      • 不要使用数据库生成的 ID。请改用代码生成的 GUID。这样做有几个好处,您可以通过 Google 快速搜索了解。
      • 没想到,但它实际上是有道理的。那会解决我的问题。谢谢。
      【解决方案3】:

      我的建议是,在控制器中使用 dbContext.SaveChanges(),因为它在网络上的所有示例中都有演示。您想要做的事情听起来很花哨,并且可能会适得其反,正如您在帖子末尾所猜测的那样。而 IMO,这没有任何意义。

      关于你的第二个问题/任务:

      ....响应 HTTP 请求时,对 DbContext 所做的所有更改都将保存到数据库中,或者不保存(例如,如果有一些异常)。

      我认为您需要“每个请求的事务”之类的东西。这只是一个想法,根本没有测试过。我只是把代码放在这个示例中间件中:

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

      祝你好运

      【讨论】:

      • 我知道它在一些示例中显示,但它们只是示例。对于真正的大型 n 层应用程序,我认为将 DbContext 注入控制器不是一个好主意,因为它破坏了关注点的分离并在服务层和持久层之间创建了依赖关系(甚至与 EntityFramework 的依赖关系)和这就是我使用存储库模式的原因。恐怕这不是一个有效的答案:)
      • 我的 DbContext 已经被限定为一个 Http 请求,并且 DbContext(从 EF 6 开始)是一个事务,当按照 msdn.microsoft.com/en-us/data/dn456843.aspx 保存更改时,这就是为什么我想知道中间件是否是一个完美的地方一次 SaveChanges(无需显式事务)。但这个想法是相似的,是的。将所有内容包装在事务中并在控制器完成执行后提交事务(保存上下文更改)
      • 您可以围绕 DbContext 实现自己的 UnitOfWork。我的评论是针对 SaveChanges 方法的,无论它是由 DbContext 还是您的 unitOfWork 对象公开都没有关系。我没有正确表达自己,因为我也不喜欢直接在控制器中注入上下文。 :)
      猜你喜欢
      • 2017-07-23
      • 1970-01-01
      • 2021-01-14
      • 2017-03-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多