Search code examples
c#.net.net-coreconcurrencyentity-framework-core

.NET A second operation was started on this context instance before a previous operation completed


I am getting this error:

System.InvalidOperationException: "A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext".

This is my code in service:

public async Task<IQueryable<GetAllUsersDTO>> UsersById(Guid userId, CancellationToken cancellationToken)
{
    try
    {
        var Log = await _db.Users
                           .Include(x => x.University)
                           .Include(x => x.Fields)
                           .Where(x => x.UserId == userId)
                           .OrderByDescending(x => x.CreatedAt)
                           .AsNoTracking()
                           .ToListAsync();
    
        var logsDTO = await Task.WhenAll(Log.Select(async x =>
        {
            var userInDb = await _db.Users.FindAsync((Guid)x.UserId); 
            var user = await _userService.GetDirectoryInformation(userInDb, cancellationToken);
            return new GetAllUsersDTO()
            {
                Description = x.Description,
                PostDate = x.PostDate,
                Id = x.Id,
                RecieveUserId = x.RecieveUserId,
                UniversityId = x.UniversityId,
                IsVisible = x.IsVisible,
                IsApproved = x.IsApproved,
                UserIdOpen = x.UserId,
                UserName = user.UserName,
                LogTitle = x.LogTitle,
                LogNr = x.LogNr,
                Email = user.Email,
            };
        }));
    
        return logsDTO.AsQueryable();
    }
    catch (Exception ex)
    {
        Console.WriteLine("Error=", ex.Message);
        throw;
    }
}

This is how I'm injecting DataContext in this service:

private readonly DataContext _db;
public UserService(DataContext context) : base(context)
{
 _db = context;
}

Note: This service only works when Log list returns only one data. But when it returns more than one data, then it triggers the error.

I have tried configuring my dependency injection setup with ServiceLifetime.Scoped, and it looks like this:

builder.Services
    .AddDbContext<DataContext>(opt =>
        opt.UseSqlServer(builder.Configuration.GetConnectionString("connString"))
        ,ServiceLifetime.Scoped
    );

Solution

  • Note: This service only works when Log list returns only one data. But when it returns more than one data, then it triggers the error.

    _db.Users.FindAsync and potentially GetDirectoryInformation (it seems that it can use/uses database too) are the problem, the following:

    var logsDTO = await Task.WhenAll(Log.Select(async x =>
    {
        var userInDb = await _db.Users.FindAsync((Guid)x.UserId); 
        var user = await _userService.GetDirectoryInformation(userInDb, cancellationToken);
        // ...
    }
    

    Will schedule to execute as many tasks as there are logs in parallel. And EF Core database context is not supposed to be used from different threads simultaneously. That's it.

    "Direct" workarounds:

    1. Move fetching all the data in the initial query (var Log = await _db.Users ...) and remove usage of _userService.GetDirectoryInformation and the Task.WhenAll(Log.Select completely (I would argue that it the best course of action)
    2. Create inner scope in every task and resolve context/_userService there

    Also note, since you gathering logs for single user there is completely no reason to query for it in multiple threads. Even if for some reason you don't want to (or can't) just use Select to build the result, i.e.:

    await _db.Users
        .Where(x => x.UserId == userId)
        .OrderByDescending(x => x.CreatedAt)
        .Select(x => new GetAllUsersDTO
        {
            Description = x.Description,
            // ...
            UserName = x.User.UserName,
            Email = x.User.Email,
        })
        .ToListAsync();
    

    Then just fetch the data one time and that's it:

    var Log = ...; // your original query
    var userInDb = await _db.Users.FindAsync(userId); 
    var user = await _userService.GetDirectoryInformation(userInDb, cancellationToken);
    var result = Logs
        .Select(x => new GetAllUsersDTO {...}) // use user, no async/awaits in the select
    // ...