Update: The threading issues were caused as a result of ApplicationDbContext being registered as Scoped, and services being registered as Transient. Registering my ApplicationDbContext as transient has fixed the threading issue. However: I do not want to lose the Unit of Work and Change Tracking funcionality. Instead I now keep the ApplicationDbContext as Scoped, and fix the issue by using Semaphore to prevent simultaneous calls, as explained in my answer here: https://stackoverflow.com/a/68486531/13678817
My Blazor Server project uses EF Core, with a complex database model (some entities having 5+ levels of child entities). When the user navigates to a new component from the nav menu, the relevant entities are loaded in OnInitializedAsync (where I inject a service for each entity type). Each service is registered as Transient in startup. The loaded entities are manipulated in this component, and in its child/nested components.
However, this approach resulted in threading issues (different threads concurrently using the same instance of DbContext) when the user would navigate between components while the previous component's services are still loading entities (...a second operation has started...).
Following is the simplified original code causing this error.
Component 1:
@page : "/bases"
@using....
@inject IBasesService basesService
@inject IPeopleService peopleService
<h1>...//Code omitted for brevity
@code{
List<Bases> bases;
List<Person> people;
protected override async Task OnInitializedAsync()
{
bases = await basesService.GetBasesAndRelatedEntities();
people = await peopleService.GetPeopleAndRelatedEntities();
}
Component 2:
@page : "/people"
@using....
@inject IBasesService basesService
@inject IPeopleService peopleService
<h1>...//Code omitted for brevity
@code{
List<Person> people;
protected override async Task OnInitializedAsync()
{
people = await peopleService.GetPeopleAndRelatedEntities();
}
Furthermore, all services have this structure, and are registered in startup as transient:
My BasesService:
public interface IBasesService
{
Task<List<Base>> Get();
Task<List<Base>> GetBasesAndRelatedEntities();
Task<Base> Get(Guid id);
Task<Base> Add(Base Base);
Task<Base> Update(Base Base);
Task<Base> Delete(Guid id);
void DetachEntity(Base Base);
}
public class BasesService : IBasesService
{
private readonly ApplicationDbContext _context;
public BasesService(ApplicationDbContext context)
{
_context = context;
}
public async Task<List<Base>> GetBasesAndRelatedEntities()
{
return await _context.Bases.Include(a => a.People).ToListAsync();
}
//...code ommitted for brevity
DbContext is registered as follows:
services.AddDbContextFactory<ApplicationDbContext>(b =>
b.UseSqlServer(
Configuration.GetConnectionString("MyDbConnection"), sqlServerOptionsAction: sqlOptions =>
{ //Updated according to https://dev-squared.com/2018/07/03/tips-for-improving-entity-framework-core-performance-with-azure-sql-databases/
sqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(5),
errorNumbersToAdd: null);
}
));
Now, my user can switch between /bases and /people by using the nav menu. If they do this quickly, then the other component's await peopleService.GetPeopleAndRelatedEntities(); gets called before the previous component's await peopleService.GetPeopleAndRelatedEntities(); has finished, and this causes an error as follows:
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
Executed DbCommand (9ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
SELECT [**Sensitive DB statement ommitted**]
fail: Microsoft.EntityFrameworkCore.Query[10100]
An exception occurred while iterating over the results of a query for context type '[**ommitted**].Data.ApplicationDbContext'.
System.InvalidOperationException: A second operation was started on this context 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.Internal.ConcurrencyDetector.EnterCriticalSection()
at Microsoft.EntityFrameworkCore.Query.Internal.SplitQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
System.InvalidOperationException: A second operation was started on this context 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.Internal.ConcurrencyDetector.EnterCriticalSection()
at Microsoft.EntityFrameworkCore.Query.Internal.SplitQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
dbug: Microsoft.Azure.SignalR.Connections.Client.Internal.WebSocketsTransport[12]
Message received. Type: Binary, size: 422, EndOfMessage: True.
dbug: Microsoft.Azure.SignalR.ServiceConnection[16]
Received 422 bytes from service 468a12a0...
warn: Microsoft.AspNetCore.Components.Server.Circuits.RemoteRenderer[100]
Unhandled exception rendering component: A second operation was started on this context 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.
System.InvalidOperationException: A second operation was started on this context 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.Internal.ConcurrencyDetector.EnterCriticalSection()
at Microsoft.EntityFrameworkCore.Query.Internal.SplitQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
at [**path to service ommitted**]...cs:line 35
at [**path to component ommitted**].razor:line 77
at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
fail: Microsoft.AspNetCore.Components.Server.Circuits.CircuitHost[111]
Unhandled exception in circuit 'cvOyWXdG_oikG_YJe2ehrsHsI3VQDJw2U8YIySmroTM'.
System.InvalidOperationException: A second operation was started on this context 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.Internal.ConcurrencyDetector.EnterCriticalSection()
at Microsoft.EntityFrameworkCore.Query.Internal.SplitQueryingEnumerable`1.AsyncEnumerator.MoveNextAsync()
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ToListAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)
at [**path to service ommitted**].cs:line 35
at [**path to component ommitted**].razor:line 77
I read through everything Stack Overflow and MS docs has available regarding this topic, and adapted my project according to the recommended approach of using DbContextFactory:
- Blazor concurrency problem using Entity Framework Core
- https://stackoverflow.com/a/58047471/13678817
Let's say I have the following DB model:
Each AlphaObject, has many BetaObjects, which has many CharlieObjects. EachCharlieObject has 1 BetaObject, and each BetaObject has 1 AlphaObject.
I adapted all services to create a new DbContext with DbContextFactory, before each operation:
public AlphaObjectsService(IDbContextFactory<ApplicationDbContext> contextFactory)
{
_contextFactory = contextFactory;
}
public async Task<List<AlphaObject>> GetAlphaObjectAndRelatedEntities()
{
using (var _context = _contextFactory.CreateDbContext())
return await _context.AlphaObjects.Include(a => a.BetaObjects).ThenInclude(b => b.CharlieObjects).ToListAsync();
}
Where before, I would load a List<AlphaObject> alphaObjects, and include all related BetaObject entities (and in turn their related CharlieObject entities) in the service, I can then later load a list of List<BetaObject> betaObjects, and without explicitly including their related AlphaObject or CharlieObjects, it would already be loaded if it was loaded before.
Now, when working with a 'DbContext per Operation' - many of my related entities are null if I don't load them again explicitly. I am also worried about manipulating entities & their related entities, and updating all these changes, without the normal lifetime of a DbContext with Change Tracking. EF Core documentation states that the normal lifetime of a DbContext should be:
- Create the DbContext instance
- Track some entities
- Make some changes to the entities
- Call SaveChanges to update the database
- Dispose the DbContext instance.
In order to solve my threading (concurrent access of the same DbContext) errors, but continue to manipulate my entities in the manner that EF Core was intended to be used:
Should I extend the lifetime of my DbContext to the lifetime of my main component in which the entities are loaded from the database?
In this way, after loading one entity, with all its related entities, I don't need to load all already loaded entities for another entity type. I would also have other benefits such as Change Tracking for the lifetime of the component, and all changes to tracked entities will be saved when calling
context.SaveChangesAsync(). However, I will need to manually dispose each context created with DbContextFacoty.As per MS Docs, it seems I would have to access the database directly from the component in order to implement IDisposable (I will need to create the Context directly in my component, and not in a service - as with the MSDocs sample app: https://learn.microsoft.com/en-us/aspnet/core/blazor/blazor-server-ef-core?view=aspnetcore-5.0). Is it really optimal/recommended to create and access the DbContext directly from within components? Otherwise, instead of implementing IDisposable, would using OwningComponentBase have the exact same capability to dispose the context after the component's lifetime ends, except I can use my existing services with this?
Can I continue to dispose my new DbContext after each service operation -
using (var _context = _contextFactory.CreateDbContext())?Then, must I simply ensure that each time I load a different entity type, I should also load all the required related entities again? I.e.
return await context.AlphaObject.Include(a => a.BetaObject).ThenInclude(b => b.CharlieObject).ToListAsync();, and when loading a list of CharlieObject, again I should explicitly include BetaObject and AlphaObject? Will I be able to still make changes to AlphaObject and it's related Beta- and CharlieObjects throughout my child components, and then when finished with all the changes, will makingcontext.Entry(AlphaObject).State = EntityState.Modified& callingcontext.SaveChangesAsync()also update changes that were made to the BetaObjects and CharlieObjects that are related to the AlphaObject? Or would one need to change the state of each entity to EntityState.Modified?
In short, I would love to understand the correct way to ensure related entities are loaded (and manipulated & updated) properly when working outside a single DbContext lifetime, as this seems to be the recommended approach. In the meantime I will go ahead an adapt my project to use new context per service operation, and continue to 'learn-as-I-go-along'. I will update this question as I learn more.
this approach resulted in concurrency issueswhy? The problems aren't caused by EF Core, they're caused by trying to store data that was already modified by another connection. This isn't a DbContext lifetime issue and can't be solved by changing the lifetime of DbContext. Post the original code that caused the problemsrowversioncolumn, EF Core will use all properties to check for changes. You don't need complex factories, don't need IDisposable, the Microsoft docs don't say anything of the kind. A DbContext is a Unit-of-Work. You create and use one when you want to perform one job/transaction/use-case/Unit-of-Work.public Task SaveAsync()=>_dbContext.SaveChangesAsync().