方法context.Orders.RemoveRange引发了InvalidOperationException。它从多个任务中调用。我试图锁定context.Orders.RemoveRange,但引发了相同的异常。
例外是:
InvalidOperationException: A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe.
这是引发异常的源代码
public class Foo : IFoo
{
private MyContext context;
public Foo(MyContext context)
{
this.context = context;
}
public async Task Update(Order order)
{
context.Orders.RemoveRange(context.Orders.Where(r => r.CustomerID == 100));
context.Orders.RemoveRange(context.Orders.Where(r => r.CustomerID == 120));
order.EmployeeID = 2;
context.Update(order);
await context.SaveChangesAsync();
}
}
异常堆栈跟踪:
at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()
at Microsoft.EntityFrameworkCore.Query.Internal.LinqOperatorProvider.ExceptionInterceptor`1.EnumeratorExceptionInterceptor.MoveNext()
at Microsoft.EntityFrameworkCore.DbContext.RemoveRange(IEnumerable`1 entities)
at WebApplication2.Foo.Update(Order order) in D:\Projects\RemoveRangeIssue\WebApplication2\Foo.cs:line 24
我将小项目添加到GitHub,以重现上述问题。这是link。它具有Task.WaitAll以在两个线程中运行两个方法。 如何在不删除Task.WaitAll的情况下从多个任务调用方法context.Orders.RemoveRange解决此问题?
答案 0 :(得分:1)
我刚刚意识到问题实际上不是您在此处显示的代码,而是您在GitHub存储库中使用的这一位:
var task1 = Task.Run(() => _foo.Update(order));
var task2 = Task.Run(() => _foo2.Update(order));
Task.WaitAll(task1, task2);
因此,在这里,您实际上有两个Foo
实现,并且您希望同时在两个实现上运行查询。由于您正在使用依赖项注入,并且它们都在同一作用域中创建,因此它们还将解析相同的数据库上下文。
通常不支持在同一数据库上下文中运行并发查询。 Entity Framework数据库上下文使用单个基础数据库连接,并且您只能同时运行一个查询。
如果您绝对需要同时运行这两个查询,那么解决方案是使用分别具有各自数据库连接的单独数据库上下文。为此,您将需要创建一个新的服务范围并从那里解析数据库上下文。
使用Microsoft.Extensions.DependencyInjection,它看起来像这样:
public class Foo : IFoo
{
private readonly IServiceScopeFactory _serviceScopeFactory;
public Foo(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
}
public async Task Update(Order order)
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var context = scope.ServiceProvider.GetService<MyContext>();
// this context is now separate from others
// …
}
}
}
您必须查看Autofac文档,以了解如何完成该操作。
或者,您也可以按原样保留Foo
实现,而不是在新范围内解析Foo
(然后从同一范围获取上下文)。这样会将服务范围的创建移到Foo
的调用方中,根据Foo
的实际职责,这可能是更好的选择。
public class ExampleController : ControllerBase
{
private readonly IServiceScopeFactory _serviceScopeFactory;
public ValuesController(IServiceScopeFactory serviceScopeFactory)
{
_serviceScopeFactory = serviceScopeFactory;
}
[HttpPost]
public ActionResult DoStuff()
{
var task1 = Task.Run(async () =>
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var foo = scope.ServiceProvider.GetService<IFoo>();
await foo.Update();
}
});
var task2 = Task.Run(async () =>
{
using (var scope = _serviceScopeFactory.CreateScope())
{
var foo = scope.ServiceProvider.GetService<IFoo>();
await foo.Update();
}
});
await Task.WhenAll(task1, task2);
return Ok();
}
}
context.Orders.Where(r => r.CustomerID == 100)
这将仅返回表示查询但尚未执行的IQueryable。然后,当您使用RemoveRange
隐式迭代该可查询对象时,将执行查询。
使用EntityFramework通常这不是一个好主意。您应该始终明确地使用ToListAsync()
或ToArrayAsync()
执行查询:
public async Task Update(Order order)
{
var ordersToRemove = await context.Orders
.Where(r => r.CustomerID == 100 || r.CustomerID == 120)
.ToListAsync();
context.Orders.RemoveRange(ordersToRemove);
order.EmployeeID = 2;
context.Update(order);
await context.SaveChangesAsync();
}