0

I'm running into this error:

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,

I have a Blazor Server app where I have a SideBar with a TreeView. Then I also load my menu items on my Index page via Cards.

The initial load is fine because the menu items are generated in memory and not the database.

On the initial login success, I am seeing the error due to the Sidebar page and then the Index page calling the same database call to get the menu items.

If I refresh the page, everything is fine. Then the menu items are added to the memory cache and work thereafter.

I've tried this link, but it did not work.

Here is my menu code:

public class MenuUtilities : IMenuUtilities
{
    private bool disposedValue;
    private readonly IMenuRepo menuRepo;
    private readonly IMemoryCache memoryCache;
    private readonly ILogger logger;

    public MenuUtilities(IMenuRepo menuRepo, IMemoryCache memoryCache, ILogger<MenuUtilities> logger) 
    {
        this.menuRepo = menuRepo;
        this.memoryCache = memoryCache;
        this.logger = logger;
    }

    public async Task<IEnumerable<MenuItemDTO>> GetMenuItems(bool isLoggedIn, Guid? userId, string fromLocation)
    {
        logger.LogInformation(message: $"fromLocation: {fromLocation} | isLoggedIn: {isLoggedIn}");
        var menuItems = new List<MenuItemDTO>();
        // Check if the item is in the cache
        if (memoryCache.TryGetValue(CacheConstants.MenuItems, out List<MenuItemDTO>? cachedItems))
        {
            // Item is in the cache, use it
            // You can access cachedItems here
            // ...
            menuItems = cachedItems;
        }
        else
        {
            menuItems.Add(new MenuItemDTO { MenuItemId = -1, MenuItemParentId = null, Name = "Home", IconTxt = "icon-microchip icon", NavigationUrlTxt = "/" });

            if (isLoggedIn)
            {
                var items = await menuRepo.GetUserMenuItems(userId).ConfigureAwait(false);
                menuItems.AddRange(items);

                memoryCache.Set(CacheConstants.MenuItems, menuItems, TimeSpan.FromMinutes(10));
            }
            else
            {
                menuItems.Add(new MenuItemDTO { MenuItemId = 10000, MenuItemParentId = null, Name = "Registration", IconTxt = "icon-circle-thin icon", NavigationUrlTxt = "/Account/Register" });
                menuItems.Add(new MenuItemDTO { MenuItemId = 10001, MenuItemParentId = null, Name = "Login", IconTxt = "bi bi-person icon", NavigationUrlTxt = "/Account/Login" });
            }
        }

        return menuItems;
    }

    protected virtual void Dispose(bool disposing)
    {
        if (!disposedValue)
        {
            if (disposing)
            {
                // TODO: dispose managed state (managed objects)
            }

            // TODO: free unmanaged resources (unmanaged objects) and override finalizer
            // TODO: set large fields to null
            disposedValue = true;
        }
    }

    public void Dispose()
    {
        // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }
}

Is there a way I can check to see if the call is still running?

I've added some logs and here are the time differences:

enter image description here

Not sure what I can add to delay the SideBar loading call?

The Sidebar page and Index page both have the same code as such:

@code {
    private IList<MenuItemInfoModel> MenuData = new List<MenuItemInfoModel>();

    protected override async Task OnInitializedAsync()
    {
        await LoadData();
    }

    private async Task LoadData()
    {
        if (menuUtilities is not null)
        {
            var menuItems = (await menuUtilities.GetMenuItems(await UserUtility.IsUserAuthenicated(AuthenticationStateProvider).ConfigureAwait(false), await UserUtility.GetCurrentUserId(AuthenticationStateProvider).ConfigureAwait(false), "cardMenu_Call").ConfigureAwait(false)).ToList();

            MenuData = mapper.Map<IList<MenuItemInfoModel>>(menuItems.ToList());
        }
    }
}

UPDATE 1

Here is the database call:

    public async Task<IList<MenuItemDTO>> GetUserMenuItems(Guid? userId)
    {
        var menuItemsWithRoles = await Context.MenuItems
            .Join(
                Context.MenuItemsToRoles,
                menuItem => menuItem.MenuItemId,
                menuItemRole => menuItemRole.MenuItemId,
                (menuItem, menuItemRole) => new { menuItem, menuItemRole }
            )
            .Join(
                Context.UserRoles,
                combined => combined.menuItemRole.RoleId,
                userRole => userRole.RoleId,
                (combined, userRole) => new { combined.menuItem, combined.menuItemRole, userRole }
            )
            .Where(combined => !userId.HasValue || combined.userRole.UserId == userId.Value)
            .Select(menuItem => new MenuItemDTO
            {
                MenuItemId = menuItem.menuItem.MenuItemId,
                MenuItemParentId = menuItem.menuItem.MenuItemParentId,
                Name = menuItem.menuItem.Name,
                DescriptionTxt = menuItem.menuItem.DescriptionTxt,
                NavigationUrlTxt = menuItem.menuItem.NavigationUrlTxt,
                IsActiveInd = menuItem.menuItem.IsActiveInd,
                SortOrder = menuItem.menuItem.SortOrder,
                IconTxt = menuItem.menuItem.IconTxt,
                CreatedById = menuItem.menuItem.CreatedById,
                CreatedByNm = $"{menuItem.menuItem.CreatedByUserNavigation.FirstName} {menuItem.menuItem.CreatedByUserNavigation.LastName}",
                CreatedDate = menuItem.menuItem.CreatedDate,
                ModifiedById = menuItem.menuItem.ModifiedById,
                ModifiedByNm = $"{menuItem.menuItem.ModifiedByUserNavigation.FirstName} {menuItem.menuItem.ModifiedByUserNavigation.LastName}",
                ModifiedDate = menuItem.menuItem.ModifiedDate,
                SoftDeletedById = menuItem.menuItem.SoftDeletedById,
                SoftDeletedByNm = GetSoftDeletedName(menuItem.menuItem),
            })
            .ToListAsync();

        foreach (var item in menuItemsWithRoles)
        {
            item.HasChildren = menuItemsWithRoles.Any(a => a.MenuItemParentId == item.MenuItemId);
        }

        return menuItemsWithRoles;
    }

Update 2:

Here is the Program.cs setup for the dbContext. I've tried all life cycles and they all cause the error ONLY on the first call. If I log out and log back in (which that code will be called in that order again at Login, then it will NOT happen.


var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");

builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlServer(connectionString));

builder.Services.AddDatabaseDeveloperPageExceptionFilter();

builder.Services.AddIdentity<ApplicationUser, IdentityRole<Guid>>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddSignInManager<SignInManager<ApplicationUser>>()
    .AddDefaultTokenProviders();

5
  • First thing to look at always: what's the life cycle of the context? Commented Jan 6, 2024 at 11:03
  • @GertArnold: I added Update 2. I've tried all the life cycles. I think scoped is by default though? Commented Jan 6, 2024 at 19:04
  • Yes, default is scoped. That should be OK, but we see only a very small part of the code. In fact, we can only guess. Commented Jan 6, 2024 at 19:55
  • @GertArnold: Understand, not sure what else I can send though without sending the entire app. Commented Jan 6, 2024 at 20:30
  • @GertArnold: I think the main issue is both calls are kicking off at the same time before it can pull from the memory cache. If there is a way I can delete the initial call for one of them to pull from the cache after the database call, then I could fix it. For now, I just handle the exception. Commented Jan 6, 2024 at 22:41

1 Answer 1

1

The first thing to check would be for any asynchronous operations not being awaited. For instance if you define an async method that is awaited, but it forgets to await a .ToListAsync() etc. on the DbContext.

The next culprit can be lazy loading. Code like:

MenuData = mapper.Map<IList<MenuItemInfoModel>>(menuItems.ToList());

or similar code consuming entities loaded from a DbContext can trigger lazy loading calls if LL is enabled. A better option if this is Automapper is to use ProjectTo on the IQueryable of entities being loaded, but this mean that any construction of a new menu (if data does not exist) would need to be done after the projection attempt. Projection with ProjectTo or Select is preferable to loading entities as any referenced entities will get composed into the query to populate the view models etc. rather than loading entities and potentially tripping lazy load calls.

Sign up to request clarification or add additional context in comments.

5 Comments

-> I added my db call. I do have ToListAsync(). I've checked throughout the other code and It seems to be await at all other places. I'll try to look at the rest of your response tomorrow.
-> I tried to look at the lazy loading, but it seems to fail even on a simple call to the database. I removed the above Menu item call and just did a simple call on the Users table and it still did the same thing.
What does your Repository code look like? How is it resolving an instance to a DbContext? Blazor Server should still have a Lifetime Scope of per-request though the request is active between calls so you do need to be more careful if there is server-side code left running as subsequent requests will be given the same DbContext instance. Anything running in the background should be given its own DbContext instance (i.e. using a ContextFactory) Beyond that something at some level not being awaited is the most likely.
Steve Py: I put the main repo code in the question. What else do you need to see?
What does "GetSoftDeletedName()" do? If this is attempting reading data from the DbContext it could cause issues as that would defer to a client-side evaluation call. A simple test would be to comment that out and just insert a dummy value. If that method is going to the DbContext to get a user's name, you will need to modify uour entity to have a navigation property for the SoftDeleteByUser and access the name via that.

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.