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.'
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();
}
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.
Here Daniel Roth (Blazor Product Manager) talking about Using Entity Framework Core with Blazor
In my case, I solved turned AddDbContext ServiceLifetime.Transient
services.AddDbContext<MY_Context>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")),
ServiceLifetime.Transient);
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
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With