Jessica
Jessica

Reputation: 2061

Multithreaded DbContext Operations

The following code (on a single DbContext) results in "A second operation started on this context before a previous operation completed".

[HttpGet]
[Route("api/[controller]/circuit")]
public async Task<IEnumerable<object>> GetAllCircuits()
{
    var circuits = await Task.WhenAll((await _context.Circuits.ToListAsync()).Select(async x => new
    {
        x.Id,
        x.FastestLap,
        x.LengthInMiles,
        Country = await _context.Countries.FirstOrDefaultAsync(c => c.Id == x.CountryId),
        Map = await _context.Maps.FirstOrDefaultAsync(m => m.Id == x.MapId),
        Locations = await _context.Locations.Where(l => l.CircuitId == x.Id).ToListAsync()
    }));

    return circuits;
}

I was able to fix this by stripping out the async/await and Task.WhenAll parts, and replacing them with .Result, which seems to be a big no-no in .NET. Fixed example below:

[HttpGet]
[Route("api/[controller]/circuit")]
public async Task<IEnumerable<object>> GetAllCircuits()
{
    var circuits = (await _context.Circuits.ToListAsync()).Select(x => new
    {
        x.Id,
        x.FastestLap,
        x.LengthInMiles,
        Country = _context.Countries.FirstOrDefaultAsync(c => c.Id == x.CountryId).Result,
        Map = _context.Maps.FirstOrDefaultAsync(m => m.Id == x.MapId).Result,
        Locations = _context.Locations.Where(l => l.CircuitId == x.Id).ToListAsync().Result
    });

    return circuits;
}

My three questions are:

  1. Why does this happen?
  2. Is the "fixed" code clean? If not, please could you suggest a better approach?
  3. Can I just use .ToList() rather than the async variant?

Thanks!

Upvotes: 0

Views: 116

Answers (1)

Stephen Cleary
Stephen Cleary

Reputation: 457147

Why does this happen?

DbContext doesn't allow multiple operations on the same db connection. In this case, you have one call (ToListAsync) followed by multiple concurrent calls (the Select).

Is the "fixed" code clean? If not, please could you suggest a better approach?

No. You shouldn't use .Result.

Your options are:

  1. (Ideal) Change the LINQ query so that it includes all the necessary information in one query, e.g., using joins or includes. This is the ideal solution because there's only one query and the db server can handle it most efficiently.
  2. Only do one operation at a time, since you only have one connection. This is why the .Result is working, but a better solution would be to use await, just do them one at a time instead of using Select with Task.WhenAll. The disadvantage to this approach is that the operations are only done one at a time.
  3. Keep the multiple operations, and open up one db connection per operation. The disadvantage to this approach is that it requires multiple db connections.

Can I just use .ToList() rather than the async variant?

The ToListAsync isn't the problem. The problem is Select + WhenAll.

Upvotes: 3

Related Questions