警惕Scope引发的事务性问题
最近在不经意之间写了一个涉及EFCore
事务性的bug。起因是某一个业务方法SubmitFlowAsync()
中要复用一段流程引擎的接口,而这个业务类中除了这个业务方法之外的其它所有方法都和流程引擎无关,所以我下意识地从之前流程引擎的单元测试中拷贝了一段程序并稍加修改,变成了下面的代码:
// 通过依赖注入得到的字段
private readonly IServiceScopeFactory _ssf;
// ...
// 一个业务方法
public async Task<FSharpResult<IEnumerable<FlowInvocationRecord>, IDomainErr>> SubmitFlowAsync(string definitionCode, string instanceCode)
{
using var scope = this._ssf.CreateScope();
var sp = scope.ServiceProvider;
IFlowDefinitionRepo defRepo = sp.GetRequiredService<IFlowDefinitionRepo>(); // 流程引擎特有接口
IFlowInstanceRepo instRepo = sp.GetRequiredService<IFlowInstanceRepo>(); // 流程引擎特有接口
var q = from definition in this.LoadDefinitionAsync(definitionCode)
from instance in this.CreateFlowInstance(instRepo, definition, Guid.NewGuid().ToString(), instanceCode)
from records in this._flowInvoker.BeginAsync(sp, instRepo, definition, instance)
.SelectError(e => {
IDomainErr err = new DummyErr("400", e.ToString() ?? this._localizer["流程启动错误"]);
return err;
})
select records;
return await q;
}
调用代码类似于:
var res = await this._dbContext.ExecuteInTransationAsync(async () =>
{
var r = await this._flowBiz.SubmitFlowAsync(
input.DefinitionCode,
input.InstanceCode);
return r;
});
但是在测试的时候发现,以上的业务代码存在事务性问题:一旦失败,无法回滚。
注意上面拿到Repository的方式:
using var scope = this._ssf.CreateScope();
var sp = scope.ServiceProvider;
IFlowDefinitionRepo defRepo = sp.GetRequiredService<IFlowDefinitionRepo>();
IFlowInstanceRepo instRepo = sp.GetRequiredService<IFlowInstanceRepo>();
这里是创建了一个新的scope,然后据之做Service Location
拿到IFlowInstanceRepo
,尽管这个Repository
会依赖DbContext
,但是和消费者的_dbContext
(往往是通过依赖注入得到)必然不是同一个DbContext
实例。所以在消费者的_dbContext
上回滚,并不会导致Repository实现所依赖的DbContext
回滚。
那么如何修复呢?移除注入的IServiceScopeFactory _ssf
,改而注入IServiceProvider _sp
:
// 直接注入 IServiceProvider
private readonly IServiceProvider _sp;
// ...
public async Task<FSharpResult<IEnumerable<FlowInvocationRecord>, IDomainErr>> SubmitFlowAsync(string definitionCode, string instanceCode)
{
// 使用注入的 _sp 拿到Repository以保证和数据库打交道的是同一个DbContext
IFlowDefinitionRepo defRepo = this._sp.GetRequiredService<IFlowDefinitionRepo>();
IFlowInstanceRepo instRepo = this._sp.GetRequiredService<IFlowInstanceRepo>();
// ...
}