Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.'

like image 870
ca9163d9 Avatar asked Oct 11 '19 18:10

ca9163d9


3 Answers

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

like image 115
dani herrera Avatar answered Oct 22 '22 03:10

dani herrera


In my case, I solved turned AddDbContext ServiceLifetime.Transient

        services.AddDbContext<MY_Context>(options =>
             options.UseSqlServer(
    Configuration.GetConnectionString("DefaultConnection")),
 ServiceLifetime.Transient);
like image 41
Diego Venâncio Avatar answered Oct 22 '22 01:10

Diego Venâncio


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
    }
}
like image 23
DrGriff Avatar answered Oct 22 '22 01:10

DrGriff