3

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?

Show in application

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()
18
  • 1
    I would start by assuming that the error message explains your problem precisely, and conduct your troubleshooting accordingly. Commented Mar 2, 2022 at 14:47
  • 3
    Please add code as text to the question, instead of as images. Commented Mar 2, 2022 at 14:47
  • This is generally a symptom of parallel/concurrent querying against the same db context. Which is not supported. It looks like your services are sharing the same context, I would generally advise against this. The doc links in the exception message is really helpful in this scenario. Commented Mar 2, 2022 at 14:49
  • Post your code in the question itself. What kind of application are you building? Blazor Server perhaps? Or have you registerd a DbContext as a singleton in a Razor Pages app? Are you trying to use a DbContext in a hosted service? Or a singleton? Commented Mar 2, 2022 at 14:50
  • How are TestService1 and TestService2 used? Are they registered as singletons? Commented Mar 2, 2022 at 14:55

1 Answer 1

11

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();
        ...
    }

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

8 Comments

Thank you very much. But when i used context factory instead of context pool i lost benefits... And I have to rewrite it in my framework. Shit. This is really the best recommended solution? I don't use context in page so its ok for me.
If you mean pooling, you can use PooledDbContextFactory with AddPooledDbFactory
Oh i missed this. Thank you very much :) So i go remake my framework. One more time. Thank you
@PetrKasnal the only reason I know about user circuits is that .... I've seen such questions here before, so I knew I had to look at the docs when I created my first Blazor Server project. I'd say you should read the docs first before starting to build a framework. Perhaps even build a couple of projects first before you start thinking about a framework, just to see what needs to be reused and what not. Logging and monitoring are important and quirky too, especially with SPAs where there's no "request". Long-lived sessions/circuits means you have to be careful of object lifetimes too
Regarding your Component Scope: Since you use the using statement, you dont have to implement IDisposable yourself. It will dispose already when exiting the method. Regarding this code: using var context=_factory.CreateContext())
|

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.