2

Here the situation I have. I created a blazor server app that manages an inventory of products. I have multiple repositories that use the same DB context in order to search or query the database entities.

In my inventory page, I have an async call that searches all the user's inventory products depending on search parameters (The search is called every time the user enters a letter in a search input field). I seem to be getting this error when the search query is called multiple times in a short amount of time:

"a second operation was started on this context instance before a previous operation completed"

Here's my Db Configuration:

 builder.Services.AddDbContext<NimaDbContext>(options =>
{
    options.UseSqlServer(builder.Configuration.GetConnectionString("NimaDbConnection"));
});

DI:

services.AddScoped<ISearcheableInventoryProductRepository<UserInventoryProductMapping>, UserInventoryProductMappingRepository>();

services.AddScoped<IUserInventoryProductMappingService, UserInventoryProductMappingService>();

Here's the call from the razor component:

private async Task SearchUserInventoryProducts(int pageNumber)
    {
        PaginationFilter.PageNumber = pageNumber;

        UserInventoryProductMappings = await _userInventoryProductMappingService
                                      .SearchByUserInventoryId(PaginationFilter, UserInventory.UserInventoryId);
    }

my service:

public async Task<PagedResponse<UserInventoryProductMappingDto>> SearchByUserInventoryId(PaginationFilter paginationFilter, int userInventoryId)
        {
            var result = await _repository.SearchByUserInventoryId(_mapper.Map<PaginationQuery>(paginationFilter), userInventoryId);

            return _mapper.Map<PagedResponse<UserInventoryProductMappingDto>>(result);
        }

my repository:

 public async Task<PagedResponse<UserInventoryProductMapping>> 

    SearchByUserInventoryId(PaginationQuery query, int userInventoryId)
            {
                try
                {
                    var defaultQuery = GetDefaultQuery().Where(x => x.UserInventoryId == userInventoryId);
    
                    if (query.SearchString != null)
                    {
                        defaultQuery = defaultQuery.Where(x => x.Product.NameLabel.LabelDescriptions.Any(x => x.Description.Contains(query.SearchString)));
                    }
    
                    if (query.SortBy != null && query.SortById != null)
                    {
                        switch (query.SortBy)
                        {
                            case "productCategory":
                                defaultQuery = defaultQuery.Where(x => x.Product.ProductCategoryId == query.SortById);
                                break;
                            case "productSubCategory":
                                defaultQuery = defaultQuery.Where(x => x.Product.ProductSubCategoryId == query.SortById);
                                break;
                        }
                    }
    
                    int count = defaultQuery.Count();
                    return new PagedResponse<UserInventoryProductMapping>
                    {
                        Data = await defaultQuery
                                .Skip((query.PageNumber - 1) * query.PageSize)
                                .Take(query.PageSize)
                                .ToListAsync(),
                        PageNumber = query.PageNumber,
                        PageSize = query.PageSize,
                        TotalPages = (int)Math.Ceiling(count / (double)query.PageSize)
    
                    };
                }
                catch (Exception e)
                {
                    _logger.LogError(e, e.Message);
    
                    throw;
                }
            }

I have made sure my queries are all awaited properly. I have also tried switching the DB context to a transient service lifetime, but without success.My services, repositories and context are using a scoped service lifetime. What am I doing wrong in this case? Thanks for helping.

Runick z
  • 23
  • 3
  • DbContext is not thread safe - it's meant to have a short lifetime and shouldn't be reused by more than one thread. Look into using scopes via IServiceScopeFactory in your repository instead. – Luke Aug 24 '22 at 14:27
  • 1
    Post your code in the question itself. When used properly, there's no way a single DbContext will be shared among requests. `AddDbContext` registers it as a *scoped* service and each request defines a different scope. A DbContext isn't a database connection and isn't meant to be shared among threads or requests – Panagiotis Kanavos Aug 24 '22 at 14:34
  • Welcome to StackOverflow! Just a pointer for asking questions: you should be copy/pasting code into the question as text using the code formatting tools and not linking to images of the code. That makes it easier for people to read as a whole and, if the user is willing or able, to debug code themselves to help. – Daevin Aug 24 '22 at 14:34
  • Does this answer your question? [EF 6 - How to correctly perform parallel queries](https://stackoverflow.com/questions/41749896/ef-6-how-to-correctly-perform-parallel-queries). Same applies to EF Core – Peter Bons Aug 24 '22 at 14:35
  • 1
    @Luke DbContext doesn't need to be wrapped in a repository, it *is* a multi-entity repository and unit-of-work. Trying to wrap it without understanding how things work results in problems like this and horribly complicated code. – Panagiotis Kanavos Aug 24 '22 at 14:36
  • Thank you, I will add the code directly in the question, my mistake. – Runick z Aug 24 '22 at 14:36
  • @PeterBons why assume there are parallel queries involved? It sounds like the OP tried to use "Best Practices™" and ended up with a singleton DbContext. A useful duplicate here would be a question showing how to use EF Core in an action without any "improvements" whatsoever. – Panagiotis Kanavos Aug 24 '22 at 14:39
  • @PanagiotisKanavos this is a hotly debated topic - I'm not encouraging the use of the repository pattern with EF Core (I don't use it myself, for similar reasons as you state). But OP chose to use the repository pattern, and I'm telling him how to fix his repository, – Luke Aug 24 '22 at 14:40
  • @Runickz looking at those screenshots is sounds like you tried to use "repositories" and "mappings" whether they were needed or not. They aren't. Not unless you intend to switch from a SQL database to MongoDB using the same interfaces. Or replace EF Core with raw ADO.NET. We can't guess what those repos or UoWs do but it's obvious somewhere DbContext is cached and reused. Try following the simple ASP.NET Core and EF Core `Getting Started` tutorials first and only try to add abstractions once you understand how things work and *after* you find an actual problem that needs abstractions – Panagiotis Kanavos Aug 24 '22 at 14:42
  • @Luke not as debated as it seems. Repositories are nice, which is why DbSet implements a single-entity repository. UoW is nice, which is why DbContext is one. It's blindly following The One And Only Pattern that's causing trouble – Panagiotis Kanavos Aug 24 '22 at 14:43
  • @PanagiotisKanavos I'm not arguing with your reasoning - I agree with you. Just giving OP advice on how he can fix it without rearchitecting his app. – Luke Aug 24 '22 at 14:45
  • I'd like to point out that as a seasoned developer I have seen a lot of EF instructional materials that do a very poor job of demonstrating the short lived nature of the DbContext. Many open and maintain one for the life of the program, because they're not trying to teach the whole system, but only parts of it. People end up where OP is because resources start at the wrong side of how to implement EF. – Logarr Aug 24 '22 at 14:48
  • Thanks guys, it seems I need to review my knowledge of ef core and the pattern i'm trying to use, but as Luke said, i'm trying to fix this issue without having to change the whole application – Runick z Aug 24 '22 at 14:49
  • @Runickz you haven't posted the code that injects or creates `_userInventoryProductMappingService` or `_repository` so one can only guess. Assuming `_userInventoryProductMappingService` is scoped and gets a scoped DbContext as a dependency (even if it's inside a "repository"), you have to use `IServiceProvider` to create a scope and *then* get a new service instance from that scope. Otherwise all requests that come from the same user will try to use the same service (and hence DbContext) that was created when the user's session started – Panagiotis Kanavos Aug 24 '22 at 14:52
  • @PanagiotisKanavos both my services and "repositories" are scoped. Thank you I will try to use this solution – Runick z Aug 24 '22 at 14:56
  • Blazor Server behaves like a desktop application. The scope is the entire user session (specifically the user circuit). After all, the server is tracking the state of a user's UI for that entire session and keeps a SignalR connection open with that user's tab – Panagiotis Kanavos Aug 24 '22 at 14:58
  • The docs have an entire article for [ASP.NET Core Blazor Server with Entity Framework Core (EF Core)](https://learn.microsoft.com/en-us/aspnet/core/blazor/blazor-server-ef-core?view=aspnetcore-6.0). Beyond that, check [ASP.NET Core Blazor dependency injection](https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-6.0#utility-base-component-classes-to-manage-a-di-scope) and the utility classes used to handle scopes. After all, you may need services scoped to the *component* instead of the session or a single operation. – Panagiotis Kanavos Aug 24 '22 at 15:03
  • @PanagiotisKanavos *why assume there are parallel queries involved?* Because that is the exact error message. A singleton context having multiple async operations running causes this error. – Peter Bons Aug 24 '22 at 15:39
  • @PeterBons the OP wasn't trying to perform queries in parallel. Nothing in the code (or initial screenshots) showed that. In a Blazor Server app the scope is the user session. The code was trying to execute *one* query at a time in response to a button click. If that happened too quickly though, the previous query was still active. The problem and solution are both documented – Panagiotis Kanavos Aug 24 '22 at 15:42

1 Answers1

5

I recommend that you review the service lifetime document for Blazor:

https://learn.microsoft.com/en-us/aspnet/core/blazor/fundamentals/dependency-injection?view=aspnetcore-6.0#service-lifetime

In Blazor, scoped services are mostly instantiated one time per user session. It is basically a singleton per user.

Changing the DBContext to transient won't do anything because the repository is still scoped and therefore the DBContext is still injected only once per session.

You will have several options, I think the easiest is to use a DBContextFactory or PooledDBContextFactory and instantiate a new context once per unit of work.

See here: https://learn.microsoft.com/en-us/ef/core/dbcontext-configuration/#using-a-dbcontext-factory-eg-for-blazor

Matt Bommicino
  • 309
  • 1
  • 5