ca9163d9
ca9163d9

Reputation: 29179

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

I'm creating a server side Blazor app. The following code is in the Startup.cs.

services.AddDbContext<MyContext>(o => o.UseSqlServer(Configuration.GetConnectionString("MyContext")), ServiceLifetime.Transient);
services.AddTransient<MyViewModel, MyViewModel>();

And in the ViewModel:

public class MyViewModel : INotifyPropertyChanged
{
    public MyViewModel(MyContext myContext)
    {
        _myContext = myContext;
    }

    public async Task<IEnumerable<Dto>> GetList(string s)
    {
        return await _myContext.Table1.where(....)....ToListAsync();
    }

And in the razor file.

@inject ViewModels.MyViewModel VM
<input id="search" type="text" @bind="search" />
<input id="search" type="button" value="Go" @onclick="SearchChanged" />   
@code {
    string search = "";
    int currentCount = 0;
    async void SearchChanged() {
        currentCount++;
        dtos = GetList(search);
    }
}

However, sometimes the following error occur when clicking the search button?

System.InvalidOperationException: 'A second operation started on this context before a previous operation completed. This is usually caused by different threads using the same instance of DbContext. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.'

Upvotes: 10

Views: 6739

Answers (4)

dani herrera
dani herrera

Reputation: 51705

Edited Aug 2020

Official guidance: https://learn.microsoft.com/ca-es/aspnet/core/blazor/blazor-server-ef-core?view=aspnetcore-3.1 with several solutions. In my opinion, the best approach on post is "Create new DbContext instances":

The recommended solution to create a new DbContext with dependencies is to use a factory.

//The factory
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;

namespace BlazorServerDbContextExample.Data
{
    public class DbContextFactory<TContext> 
        : IDbContextFactory<TContext> where TContext : DbContext
    {
        private readonly IServiceProvider provider;

        public DbContextFactory(IServiceProvider provider)
        {
            this.provider = provider;
        }

        public TContext CreateDbContext()
        {
            if (provider == null)
            {
                throw new InvalidOperationException(
                    $"You must configure an instance of IServiceProvider");
            }

            return ActivatorUtilities.CreateInstance<TContext>(provider);
        }
    }
}

Injecting the factory:

services.AddDbContextFactory<ContactContext>(opt =>
    opt.UseSqlite($"Data Source={nameof(ContactContext.ContactsDb)}.db")
    .EnableSensitiveDataLogging());

Using the factory:

private async Task DeleteContactAsync()
{
    using var context = DbFactory.CreateDbContext();

    Filters.Loading = true;

    var contact = await context.Contacts.FirstAsync(
        c => c.Id == Wrapper.DeleteRequestId);

    if (contact != null)
    {
        context.Contacts.Remove(contact);
        await context.SaveChangesAsync();
    }

    Filters.Loading = false;

    await ReloadAsync();
}

My deprecated answer:

You can try to create a new scope for each request:

public class MyViewModel : INotifyPropertyChanged
{
    
    protected readonly IServiceScopeFactory _ServiceScopeFactory;

    public MyViewModel(IServiceScopeFactory serviceScopeFactory)
    {
        _ServiceScopeFactory = serviceScopeFactory;
    }

    public async Task<IEnumerable<Dto>> GetList(string s)
    {
        using (var scope = _ServiceScopeFactory.CreateScope())
        {
            var referenceContext = scope.ServiceProvider.GetService<MyContext>();    
            return await _myContext.Table1.where(....)....ToListAsync();
        }
    }

In the following screenshot you can see a sample case of this issue. User clicks quickly in several pagination elements. A new request starts before previous one is ended.

Screenshot: it shows how a new request starts before previous one ends

Here Daniel Roth (Blazor Product Manager) talking about Using Entity Framework Core with Blazor

Upvotes: 15

Diego Ven&#226;ncio
Diego Ven&#226;ncio

Reputation: 6017

In my case, I solved turned AddDbContext ServiceLifetime.Transient

        services.AddDbContext<MY_Context>(options =>
             options.UseSqlServer(
    Configuration.GetConnectionString("DefaultConnection")),
 ServiceLifetime.Transient);

Upvotes: 3

Ali Borjian
Ali Borjian

Reputation: 1108

I resolve the issue however I think I have lost the Unit Of Work because now I have more than one dbContex :

Constructor :

private AppDbContext _db;
protected override void OnInitialized()
{
    _db = new AppDbContext();
     var query = _db.Set<Group>().AsQueryable();
}

and later I dispose it:

public void Dispose()
{
    _db?.Dispose();
}

Upvotes: 0

DrGriff
DrGriff

Reputation: 4916

The error message is to do with the fact the EF context can't perform more than one operation at a time.

My understanding is that if you're on a page, then you have a constant connection through to the "Service" file via a SingalR connection.

If your page makes multiple calls through to the Service, then it could be that the Context is being called to perform an operation before it has completed the previous one.

Rather than have one instance of the Context for the lifetime of the Service, I've create one instance per call. It seems to mitigate this problem, but whether it's seen as "best practice" I'm not yet sure.

So, for example:

public class MyService
{
    private MyContext Context => new MyContext(new DbContextOptions<MyContext>()));

    private async Task DoSomething()
    {
        await using var context = this.Context;  //New context for the lifetime of this method
        var r = await context.Something
            .Where(d => d....)
            .AsNoTracking()
            .FirstOrDefaultAsync()
            .ConfigureAwait(false);

         // context gets disposed of
         // Other code
    }
    private async Task DoSomethingElse()
    {
        await using var context = this.Context;   //New context for the lifetime of this method
        var r = await context.Something
            .Where(d => d....)
            .AsNoTracking()
            .FirstOrDefaultAsync()
            .ConfigureAwait(false);

         // context gets disposed of
         // Other code
    }
}

Upvotes: 2

Related Questions