方法context.Orders.RemoveRange引发了InvalidOperationException

时间:2019-12-21 07:37:06

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

方法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解决此问题?

1 个答案:

答案 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();
}