graphql-dotnet: Using scoped DbContext in DataLoaders - System.InvalidOperationException: A second operation started on this context before a previous operation completed

Summary

When using a scoped DbContext with multiple DataLoaders all resolving at the same time, I get the following error:

GraphQL.ExecutionError: Error trying to resolve agent. ---> System.InvalidOperationException: A second operation started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe.\n at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.EnterCriticalSection()\n...

I can’t find any documentation or issues directly related to this, which leads me to believe that I’m using it wrong.

One obvious solution is to change my service registration for the DbContext to Transient, instead of scoped (default), but this will require a large refactor and I have seen a few references that all strongly discourage this.

Any ideas?

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Comments: 22 (4 by maintainers)

Commits related to this issue

Most upvoted comments

you can work around this by implementing your own execution strategy

https://github.com/graphql-dotnet/graphql-dotnet/pull/1026

public class EfDocumentExecuter : DocumentExecuter
{
    protected override IExecutionStrategy SelectExecutionStrategy(ExecutionContext context)
    {
        if (context.Operation.OperationType == OperationType.Query)
        {
            return new SerialExecutionStrategy();
        }
        return base.SelectExecutionStrategy(context);
    }
}

Unfortunately, DbContext is not capable of doing multiple requests at the same time. Using transient DbContext is an option, but you have to carefully dispose of them later, and usage of DbContextFactory may be broken. You may also try creating a custom context in your resolve methods (or somewhere in you dataloader methods) and use it to work with DbContext.

    Field<UserType>(
                "getUser",
                arguments: new QueryArguments(
                    new QueryArgument<NonNullGraphType<StringGraphType>> {Name = "name"}
                ),
                resolve: context =>
                {
                    var name = context.GetArgument("name", "");
                    using (var scope = _serviceProvider.CreateScope())
                    {
                        var dbContext = scope.ServiceProvider.GetService<DbContext>();
                        var user = await dbContext.Users.FirstAsync(e => e.Name == name);
                        return user;
                    }
                });

I solved this by injecting Func<DbContext> instead of DbContext and instantiate a new DbContext every time I need one. This way DbContext is never shared across multiple threads.

Register Func<DbContext> in Startup.cs after MyDbContext is registered:

            services.AddDbContext<MyDbContext>(options => options.UseSqlServer(Configuration.GetSection("ConnectionStrings")?["DefaultConnection"]), ServiceLifetime.Transient);
            services.AddTransient<Func<MyDbContext>>(options => () => options.GetService<MyDbContext>());

Use Func<DbContext> in Queries. Example:

    public class PostType : ObjectGraphType<Post>
    {
        public PostType(Func<MyDbContext> dataContext)
        {
            Field(t => t.Id);
            Field(t => t.CreatedAt);

            FieldAsync<NonNullGraphType<UserType>>(
                "user",
                resolve: async context =>
                {
                    using (var dc = dataContext())
                        return await dc
                            .Users
                            .FirstAsync(u => u.Id == context.Source.UserId);
                });

            FieldAsync <NonNullGraphType<ListGraphType<CommentType>>>(
                "comments",
                resolve: async context =>
                {
                    using (var dc = dataContext())
                        return await dc
                            .Comments
                            .Where(c => c.PostId == context.Source.Id)
                            .OrderBy(c => c.CreatedAt)
                            .ToListAsync();
                });
        }
    }

I’m not sure if I configured this properly, so I’d love to get some feedback on this.

Hello, I have the same issue. Can someone give a complete example of the execution strategy class along with their startup.cs file please.

Thanks