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

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

  1. 核心层是独立的(不了解 EFCore 或任何其他项目)
  2. 数据层 - 引用 Core 和 EFCore
  3. 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 类时,事务就已经开始了

编辑:可能的解决方案:

  1. 创建了一个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();
            }
        }
    }   
    
  2. 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 动词是一个好主意和可行的选择。

标签: c# .net asp.net-core-mvc entity-framework-core asp.net-core-webapi


【解决方案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);
        }
    }
}

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

【讨论】:

    猜你喜欢
    • 2020-01-31
    • 1970-01-01
    • 2018-12-22
    • 1970-01-01
    • 2019-02-17
    • 2023-03-25
    • 1970-01-01
    • 2016-04-23
    • 2020-11-23
    相关资源
    最近更新 更多