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();
在UseHttpsRedirection
和UseMvc
之前注入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 => options.Filters.AddService<CustomActionFilter>();
?
不是过滤器,是自定义中间件。
@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。