2

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:

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:

  1. 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?

  2. 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 making context.Entry(AlphaObject).State = EntityState.Modified & calling context.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.

18
  • 1
    There is no right answer. It depends on the size of the database and the time to query the database. If time to read the database is 10 seconds you do not want to read from database before every query. So you want to read from database only once. If time to read from database is only 1 second than you may want to read before every query. Commented Jul 20, 2021 at 9:53
  • 1
    this approach resulted in concurrency issues why? 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 problems Commented Jul 20, 2021 at 10:30
  • 1
    When you work in a disconnected mode, whether using DataTable, a List<Product> or a DbContext, when you try to same any changes you made to your in-memory data, there's always a small chance that the table rows may have been changed by another application/user/connection. The chance is small, because each user or connection typically works with different data. So why would your code throw a concurrency exception all the time? How did all those rows end up being modified by your code at the same time some other connection changed them? That's highly unusual Commented Jul 20, 2021 at 10:33
  • 1
    Are you detaching and reattaching entities instead of modifying the entities you already loaded? In that case EF Core won't know which properties have changed or what their original values were, so concurrency checks may fail. If your table has no rowversion column, 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. Commented Jul 20, 2021 at 10:43
  • 1
    If you want to load some rows, edit them in a grid and save the changes, you have one unit-of-work. This means you need one DbContext instance for the duration of that operation. Since the scope in Blazor Server is the entire user session, you can create a DbContext when you open the Edit page and close it only when the user navigates away. You don't need to detach and re-attach the entities you loaded. All you'd need to save all modifications made in the page would be a button calling public Task SaveAsync()=>_dbContext.SaveChangesAsync(). Commented Jul 20, 2021 at 10:45

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.