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

Ish*_*mas 7 .net c# entity-framework-core asp.net-core-mvc asp.net-core-webapi

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

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

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

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

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

    [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();
    }
Run Code Online (Sandbox Code Playgroud)

一切都很好。那么问题出在哪里呢?我需要重复一遍:

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


    }
Run Code Online (Sandbox Code Playgroud)

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

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

编辑:可能的解决方案:

  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();
            }
        }
    }   
    
    Run Code Online (Sandbox Code Playgroud)
  2. 在之后和之前注入UnitOfWork类作为中间件:UseHttpsRedirectionUseMvc

    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();
    }
    
    Run Code Online (Sandbox Code Playgroud)

Chr*_*uer 5

我遇到了类似的问题,并根据 @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()));
Run Code Online (Sandbox Code Playgroud)

当 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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

希望这对某人有帮助;)