Reputation: 185
I have problem with ef core. I have two services which read data from database. On one page is call first service and on second page is called second service. When i click to button for create a new program i got error. I call it normally from page with inject service. Can anybody help me with it?
builder.Services.AddDbContextPool<Context>(options =>
{
options.UseSqlServer(builder.Configuration.GetConnectionString("Connection"));
});
TestService1:
public class TestService1 : ITestService1
{
private readonly Context _context;
private readonly IMapper _mapper;
public TestService1(Context context, IMapper mapper)
{
_kreativgangContext = kreativgangContext;
_mapper = mapper;
}
public virtual async Task<AllProgramViewModel> HandleAsync(AllProgramFilterViewModel filter)
{
var model = new AllProgramViewModel();
var data = _context.Programs.Where(x => (EF.Functions.Like(x.Name ?? "", "%" + filter.Name + "%") || string.IsNullOrEmpty(filter.Name)))
.Select(x => new Core.Models.Program() { ID = x.ID, Name = x.Name, Order = x.Order });
result.Model.TotalCount = await data.CountAsync();
result.Model.Items = data.Select(x => _mapper.Map<AllProgramItemViewModel>(x));
return model;
}
}
public interface ITestService1
{
public Task<AllProgramViewModel> HandleAsync(AllProgramFilterViewModel filter);
}
Test service 2:
public class TestService2 : ITestService2
{
private readonly Context _context;
public TestService2(Context context)
{
_context = context;
}
public virtual async Task<NewProgramViewModel> HandleAsync()
{
var model = new NewProgramViewModel();
List<ProgramOrderViewModel> items = _context.Programs.Select(x => new Core.Models.Program() { Order = x.Order, ID = x.ID })
.Select(x => new ProgramOrderViewModel()
{
ID = x.ID,
Order = x.Order
}).ToList();
return await Task.FromResult(model);
}
}
public interface ITestService2
{
public Task<NewProgramViewModel> HandleAsync();
}
Error:
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. For more information on how to avoid threading issues with DbContext, see https://go.microsoft.com/fwlink/?linkid=2097913.
at Microsoft.EntityFrameworkCore.Infrastructure.Internal.ConcurrencyDetector.EnterCriticalSection()
at Microsoft.EntityFrameworkCore.Query.Internal.SingleQueryingEnumerable`1.Enumerator.MoveNext()
at System.Collections.Generic.List`1..ctor(IEnumerable`1 collection)
at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
at Mitar.Kreativgang.Admin.Handlers.TestService2.HandleAsync() in D:\Programming\Kreativgang\Src\Mitar.Kreativgang.Admin\Handlers\TestService2.cs:line 26
at Mitar.Kreativgang.Admin.Pages.Program.ProgramNew.OnInitializedAsync() in D:\Programming\Kreativgang\Src\Mitar.Kreativgang.Admin\Pages\Program\ProgramNew.razor:line 114
at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
Upvotes: 3
Views: 7654
Reputation: 131581
This is a known and documented pitfall, explained in ASP.NET Core Blazor Server with Entity Framework Core (EFCore). In Blazor Server, the DI scope is the user circuit - essentially the user session. That means that a scoped
service like TestService2
or a DbContext
will remain in memory for a long time and end up reused by multiple methods and actions.
As the docs explain :
Blazor Server is a stateful app framework. The app maintains an ongoing connection to the server, and the user's state is held in the server's memory in a circuit. One example of user state is data held in dependency injection (DI) service instances that are scoped to the circuit. The unique application model that Blazor Server provides requires a special approach to use Entity Framework Core.
You need to register and use a DbContextFactory (or PooledDbContextFactory) instead of a DbContextPool, and create a new DbContext instance right where it's used.
builder.Services.AddDbContextFactory<ContactContext>(opt =>
opt.UseSqlServer(...));
or
builder.Services.AddPooledDbContextFactory<ContactContext>(opt =>
opt.UseSqlServer(...));
The service constructors should accept the factory instead of a context :
public TestService2(AddDbContextFactory<ContactContext> factory)
{
_factory = factory;
}
public virtual async Task<NewProgramViewModel> HandleAsync()
{
using var context=_factory.CreateContext())
{
...
}
}
Component Scope
To limit a DbContext's scope to a single component it's not enough to just inject the DbContextFactory. The DbContext instance needs to be explicitly disposed when the user navigates away from the component. To do this, the component needs to implement IDisposable. This is explained in the section Scope to the component lifetime
@implements IDisposable
@inject IDbContextFactory<ContactContext> DbFactory
...
@code
{
ContactContext? Context;
public void Dispose()
{
Context?.Dispose();
}
protected override async Task OnInitializedAsync()
{
Context = DbFactory.CreateDbContext();
...
}
}
Upvotes: 10