14

I have a WebApi controller in a Dotnet Core project running Entity Framework Core with Sqlite.

This code in an action occationally produces errors:

var t1 = _dbContext.Awesome.FirstOrDefaultAsync(a => [...]);
var t2 = _dbContext.Bazinga.FirstOrDefaultAsync(b => [...]);
var r1 = await t1;
var r2 = await t2;

The errors have been:

  • Microsoft.EntityFrameworkCore.Query.RelationalQueryCompilationContextFactory:Error: An exception occurred in the database while iterating the results of a query. System.ObjectDisposedException: Safe handle has been closed

  • Microsoft.EntityFrameworkCore.Query.RelationalQueryCompilationContextFactory:Error: An exception occurred in the database while iterating the results of a query. System.InvalidOperationException: ExecuteReader can only be called when the connection is open.

Both errors to me suggests somethings going on with the DbContext, like premature disposing (albeit not of the DbContext itself). The DbContext is being injected in the controller constructor using the plumbing of Dotnet Core "the usual way", configuration as shown below (from my Startup.cs ConfigureServices method body):

services.AddDbContext<ApplicationContext>(options => options.UseSqlite(connectionString));

If I change the error producing code above to something like:

var r1 = await _dbContext.Awesome.FirstOrDefaultAsync(a => [...]);
var r2 = await _dbContext.Bazinga.FirstOrDefaultAsync(b => [...]);

... I haven't seen the errors mentioned, hence the conclusion that running multiple tasks concurrently on the same instance of my DbContext (injected as described above) is what's causing the issue. Obviously, it's an unexpected one.

Questions:

  1. Have I come to the right conclusion, or is there something else going on?
  2. Can you pinpoint why the errors occur only occasionally?
  3. Do you know of any simple way to avoid the issue while still running concurrent tasks on the DbContext?
Oskar Lindberg
  • 2,255
  • 2
  • 16
  • 36

2 Answers2

23

Unfortunately you can't do that.

From the EF Core documentation

EF Core does not support multiple parallel operations being run on the same context instance. You should always wait for an operation to complete before beginning the next operation. This is typically done by using the await keyword on each asynchronous operation.

Also from the EF 6 documentation

Thread Safety

While thread safety would make async more useful it is an orthogonal feature. It is unclear that we could ever implement support for it in the most general case, given that EF interacts with a graph composed of user code to maintain state and there aren't easy ways to ensure that this code is also thread safe.

For the moment, EF will detect if the developer attempts to execute two async operations at one time and throw.


A DbContext only supports a single open data reader at any point in time. If you want to execute multiple simultaneous database queries you will need multiple DbContext instances, one for each concurrent query.

As to why the error occurs occasionally, its a race condition. Just because you start 2 tasks one after the next (without awaiting) does not guarantee that the database will be hit at the same time. Sometimes the execution times happen to line up and other times one task might finish right as the other task starts so there is no conflict.

How to avoid it - don't do it as its not supported. Await each of your DbContext calls or use multiple DbContext instances.

by query I mean any DB operation including SELECT, UPDATE, DELETE, INSERT, ALTER, STORED PROCEDURE CALL, ETC

Community
  • 1
  • 1
Igor
  • 60,821
  • 10
  • 100
  • 175
  • I understand, and I realize, of course, I can avoid it by not doing it ;) I see that you've involved in similar questions before. I didn't find them before 'cause I didn't get what to look for. Thanks especially for the documentation reference - kind of a pitfall, this one. I'm curious as to why I did not receive the explicit "don't do concurrent async" error mentioned. – Oskar Lindberg Apr 19 '17 at 13:45
  • 1
    @OskarLindberg - EF6 has a check built into it to try to detect if it is being used in this manner and will fail with a proper error message. I am guessing they did not build this same check into EF-Core so it fails as soon as there is a conflict but if the 2 (or more tasks) happen to just pass each other safely then (*luckily?*) the execution calls succeed. – Igor Apr 19 '17 at 13:48
  • 3
    @Igor You might also want to link to the [EF Core documentation](https://learn.microsoft.com/en-us/ef/core/querying/async) which states that parallel queries are not supported. – cremor May 14 '17 at 08:40
0

Certainly EF does not support multiple parallel operations being run on the same context instance, as per documentation, however you can create anothers contexts using DbContextOptions for same dbcontext

   private readonly DbContextOptions<ApplicationDbContext> _dbOptions;

then you can use in your controller

  public MyController(DbContextOptions<ApplicationDbContext> dbOptions)
    {
        _dbOptions = dbOptions;
    }

Now in your method you can call task, each one will use their own context, for example

public async Task<IActionResult> MyView()
    {
        var dataAwesome = ParallelAwesome();
        var dataBazinga = ParallelBazinga();
     
        await Task.WhenAll(dataAwesome, dataBazinga);
        // here we wait for all of them, once their ar done
        // you can get data from Result
        r1=dataAwesome.Result;
        r2=dataBazinga.Result;
        // do what you want with r1 and r2
        return MyView();

    }

And you have to code the following functions

private Task<Awesome> ParallelAwesome()
    {
        var localContext = new ApplicationDbContext(_dbOptions);

        var result = localContext.Awesome.FirstOrDefaultAsync(a => [...]); // notice we do not use await
        return result;        
    }

private Task<Bazinga> ParallelBazinga()
    {
        var localContext = new ApplicationDbContext(_dbOptions);

        var result = localContext.Bazinga.FirstOrDefaultAsync(a => [...]);
        // notice we do not use await
        return result;        
    }