ASP.NET Core Web API - 如何在中间件管道中隐藏 DbContext 事务?

Posted

技术标签:

【中文标题】ASP.NET Core Web API - 如何在中间件管道中隐藏 DbContext 事务?【英文标题】:ASP.NET Core Web API - How to hide DbContext transaction in the middleware pipeline? 【发布时间】:2020-02-02 03:01:55 【问题描述】:

我正在构建 3 层 ASP.NET Core Web API。它由数据、业务(核心)和 WebAPI 层组成:

    核心层是独立的(不了解 EFCore 或任何其他项目) 数据层 - 引用 Core 和 EFCore WebAPI 层 - 最后一层,了解 Core、Data 和 EFCore

我正在努力决定如何处理数据库事务。我将 DbContext(作用域)注入到我的 Data 类和控制器(我省略了业务项目,因为它根本不知道 EFCore)。因为每个请求只有一个 DbContext 实例,所以它在 Data 和 Controller 中是同一个对象。

所以,业务逻辑正在做什么,应该做什么,调用数据层中的对象。每当数据层需要将更改保存到数据库中时,它都会这样做。一切都围绕着每个请求的事务。因此,如果出现问题...所有更改都会回滚。

这是显示我是如何做到的示例控制器的方法(简化):

    [HttpPut("id")]
    public IActionResult UpdateMeeting(int id, [FromBody] MeetingDto meeting)
    
        using (var transaction = _dbContext.Database.BeginTransaction())
        
            if (meeting == null)
            
                return BadRequest();
            

            _meetingService.AddMeetingChanges(meeting);

            meeting.Id = id;
            _meetingService.UpdateMeeting(meeting);
        
        return NoContent();
    

一切都很好。那么问题是什么?我需要重复一遍:

    using (var transaction = _dbContext.Database.BeginTransaction())
    


    

...在每个操作中,都需要事务。

所以我在想,是否可以在中间件/管道中启动事务(我不确定术语)。简单地说 - 我想根据每个请求明确地开始交易。我想隐藏在中间件中。这样每当我将 DbContext 注入 Data 类时,事务就已经开始了

编辑:可能的解决方案:

    创建了一个UnitOfWork 类:

    public class UnitOfWork
    
        private readonly RequestDelegate _next; 
    
        public UnitOfWork(RequestDelegate next)
        
            _next = next;
           
    
        public async Task Invoke(HttpContext httpContext, MyContext ctx)
        
            using (var transaction = ctx.Database.BeginTransaction())
            
                await _next(httpContext);
                transaction.Commit();
            
        
       
    

    UseHttpsRedirectionUseMvc 之前注入UnitOfWork 类作为中间件:

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    
        if (env.IsDevelopment())
        
            app.UseDeveloperExceptionPage();
        
        else
        
            app.UseExceptionHandler(appBuilder =>
            
                appBuilder.Run(async context =>
                
                    context.Response.StatusCode = 500;
                    await context.Response.WriteAsync("An unexpected error happened. Please contact IT.");
                );
            );
           
    
        app.UseHttpsRedirection();
        app.UseMiddleware<UnitOfWork>();
        app.UseMvc();
    
    

【问题讨论】:

使用中间件,注入上下文,在请求中包含一个可以由中间件重新检查的标志,以指示它应该在事务中包装管道中的下一个。确保中间件在管道中及早注册。 @Nkosi 谢谢。澄清一下,您的意思是:将 DbContext 注入实现IAsyncActionFilter 的类,然后将其添加到 Mvc 中?像这样:services.AddMvc(options =&gt; options.Filters.AddService&lt;CustomActionFilter&gt;(); ? 不是过滤器,是自定义中间件。 @Nkosi 哦,我明白了。我不确定让 API 的客户决定他是否要使用事务是否是个好主意。我想我会为每个请求创建事务。不好吗?我可以检查请求是否是“GET”,如果是,我不会开始交易。 这只是一个想法,并不难。使用 HTTP 动词是一个好主意和可行的选择。 【参考方案1】:

我遇到了类似的问题,并在 @Ish Thomas 的建议和解决方案之上构建了一个中间件。 万一有人发现这个问题,我想把我的中间件解决方案留在这里。

但不幸的是,我还不得不使用 EF Connection Resiliency 配置EnableRetryOnFailure()。 此配置与ctx.Database.BeginTransaction() 不兼容并抛出InvalidOperationException

InvalidOperationException:配置的执行策略“SqlServerRetryingExecutionStrategy”不支持用户发起的事务。使用 'DbContext.Database.CreateExecutionStrategy()' 返回的执行策略将事务中的所有操作作为可重试单元执行。

services.AddDbContext<DemoContext>(
        options => options.UseSqlServer(
            "<connection string>",
            providerOptions => providerOptions.EnableRetryOnFailure()));

当 HTTP 动词是 POST、PUT 或 DELETE 时,中间件会创建一个事务。 否则,它会在没有事务的情况下调用下一个中间件。 如果抛出异常,则不会执行事务提交,并且会回滚在此请求中所做的更改。

中间件代码:

public class TransactionUnitMiddleware

    private readonly RequestDelegate next;

    public TransactionUnitMiddleware(RequestDelegate next)
    
        this.next = next;
    

    public async Task Invoke(HttpContext httpContext, DemoContext context)
    
        string httpVerb = httpContext.Request.Method.ToUpper();

        if (httpVerb == "POST" || httpVerb == "PUT" || httpVerb == "DELETE")
        
            var strategy = context.Database.CreateExecutionStrategy();
            await strategy.ExecuteAsync<object, object>(null!, operation: async (dbctx, state, cancel) =>
            
                // start the transaction
                await using var transaction = await context.Database.BeginTransactionAsync();

                // invoke next middleware 
                await next(httpContext);

                // commit the transaction
                await transaction.CommitAsync();

                return null!;
            , null);
        
        else
        
            await next(httpContext);
        
    

希望这对某人有所帮助;)

【讨论】:

以上是关于ASP.NET Core Web API - 如何在中间件管道中隐藏 DbContext 事务?的主要内容,如果未能解决你的问题,请参考以下文章

带有 EF Core 更新实体的 ASP.Net 核心 Web Api 如何

如何在 ASP.NET Core Web API 中发布对象列表

Asp.Net Core Web API 应用程序:如何更改监听地址?

ASP.Net Core Web API 如何返回 File。

如何使 ASP.NET Core Web API 操作异步执行?

如何注销或过期 ASP.NET Core Web API 的 JWT 令牌?