“A second operation started on this context” EF core 3.1 concurrency breaking change

.net-core async-await c# entity-framework-core

Question

I'm migrating from netcoreapp 2.1 to 3.1, and I've found a breaking change for EF core 3.1 that I can't resolve. The following worked in 2.1 so simply saying db context isn't thread safe by design and pointing to other questions that don't deal with this nuance does not address the issue at hand.

Previously in 2.1, this worked:

taskList.Add(MethodOne(myRequestObject));
taskList.Add(MethodTwo(myRequestObject));

await Task.WhenAll(taskList);

where both methods only read(never changed) from the db context and look something like this:

private async Task MethodOne(RequestObject myRequestObject)
{
    var entity = await DbContext
    .MyDbSet
    .OrderByDescending(x => x.SomeProperty)
    .FirstOrDefaultAsync(x => x.Id == myRequestObject.Id);

    if (entity != null)
        myRequestObject.SomeRequestProperty = entity.AnotherProperty;
    }
}

In 3.1, even when I'm just reading and not changing the entities, the DB context ConcurrencyDetector thinks it has EnterCriticalSection which causes an exception when the second method tries to await on the DbContext:

InvalidOperationException: A second operation started on this context before a previous operation completed

For baseline sanity, I also tried the following which does work(but is not an ideal solution for the real code of my app):

await MethodOne(myRequestObject);
await MethodTwo(myRequestObject);

So here is my questions:

  1. Is there a way to continue to tell EF core 3.1 to allow concurrency when I know it is safe. this worked in 2.1 so clearly this cannot be dismissed by simply saying db context has never allowed this by design and closing the question as a duplicate. My app has been running happily in production for a long time. Only after migrating to 3.1 has this become an issue. What has changed? Is this change truly impossible to work around or are there exceptions to the claim that the context doesn't allow this? If this worked before when this 'wasn't allowed by design', is it possible that it is similarly not true now as well?
1
-2
3/6/2020 6:59:13 PM

Accepted Answer

Is there a way to continue to tell EF core 3.1 to allow concurrency when I know it is safe. this worked in 2.1 so clearly this cannot be dismissed by simply saying db context has never allowed this by design and closing the question as a duplicate.

The DB context has never allowed that. I've never seen this kind of code work on any version of .NET Core. If it happened to work in 2.x, it was only "working" in the sense that the code was winning its race conditions and thus just getting lucky. Any change in performance - reduced memory, antivirus software, alternate network routing - could have caused this to fail. It's very important to recognize that the code is wrong and always has been.

My app has been running happily in production for a long time. Only after migrating to 3.1 has this become an issue. What has changed?

Probably some timing in the framework.

If this worked before when this 'wasn't allowed by design', is it possible that it is similarly not true now as well?

The race condition still exists. If your code happens to win the race condition, you won't see that exception.

Is this change truly impossible to work around or are there exceptions to the claim that the context doesn't allow this?

There's no workaround to force a dbcontext to work with multiple simultaneous requests. The normal pattern for doing simultaneous requests is to use multiple dbcontexts, one for each simultaneous request.

2
3/6/2020 11:24:09 PM

Popular Answer

A DbContext is cheap to create. By default, if you're injecting them into a scoped service, a new one is created for every request. But you can also create them yourself, while still leveraging .NET Core's DI framework.

Declare your DbContext in Startup.cs as you normally would. But the service (you are injecting services into your controllers, right?) that uses your DbContext can be a singleton instead of scoped, since it doesn't need to be injected with a new DbContext per-request. The background task started by the service can then use a ScopeFactory to create contexts as needed:

using (var scope = ScopeFactory.CreateScope())
using (var db = scope.ServiceProvider.GetRequiredService<MyDbContext>())
{
    // do stuff with db
}


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow