简体   繁体   中英

Entity Framework query throws 'async error' after many requests

In my project using .NET framework 4.6.1, EF 6.1.4 and IdentityServer3, I set the following DbContext:

public class ValueContext : DbContext
{
    public IValueContext(bool lazyLoadingEnabled = false) : base("MyConnectionString")
    {
        Database.SetInitializer<IValueContext>(null);
        Configuration.LazyLoadingEnabled = lazyLoadingEnabled;
    }

    public DbSet<NetworkUser> NetworkUser { get; set; }
    public DbSet<User> User { get; set; }

[...]

And my Entity model User :

[Table("shared.tb_usuarios")]
public class NetworkUser
{
    [Column("id")]
    [Key()]
    public int Id { get; set; }

    [Required]
    [StringLength(255)]
    [Column("email")]
    public string Email { get; set; }

    [...]
    public virtual Office Office { get; set; }
    [...]

So far I think its all good.

Then I set this following query in my UserRepository (using DI)

    protected readonly ValueContext Db;
    public RepositoryBase(ValueContext db)
    {
        Db = db;
    }

    public async Task<ImobUser> GetUser(string email)
    {
        //sometimes I get some error here
        return await Db.User.AsNoTracking()
          .Include(im => im.Office)
          .Include(off => off.Office.Agency)
          .Where(u => u.Email == email &&
                      u.Office.Agency.Active)
          .FirstOrDefaultAsync();
    }

And everything runs well, until it starts to get many sequential requests , then I begin to get these type of errors, randomly in any function that uses my ValueContext as data source:

System.NotSupportedException: 'A second operation started on this context before a previous asynchronous operation completed. Use 'await' to ensure that any asynchronous operations have completed before calling another method on this context. Any instance members are not guaranteed to be thread safe.'

This is my last hope, as I tried a bunch of different things. Some of them work, and some dont, like:

  • Convert dbContext to use DI: no difference.
  • Use context lifetime to run the queries: works, but isnt the solution I want.
  • Remove asyncronous from requests: works, but also I feel is not the correct way to do.

What Im doing wrong?

EDIT 1

This is how I set up DI in Startup.cs :

 private void AddAuth()
 {            
        Builder.Map("/identity", app =>
        {
            var factory = new IdentityServerServiceFactory()
            {
               //here I implemented the IdentityServer services to work
               ClientStore = new Registration<IClientStore>(typeof(ClientStore)),
               [...]
            };

            AddDependencyInjector(factory);
        }

        [...]

}

private void AddDependencyInjector(IdentityServerServiceFactory factory)
{     
      //here I inject all the services I need, as my DbContext       
      factory.Register(new Registration<ValueContext>(typeof(ValueContext)));
      [...]

}

And this is how my UserService is working:

 public class UserService : IUserService
 {

    [Service injection goes here]
    
    //this is a identityServer method using my dbContext implementation on UserRepository
    public async Task AuthenticateLocalAsync(LocalAuthenticationContext context)
    {
    
        SystemType clientId;
        Enum.TryParse(context.SignInMessage.ClientId, true, out clientId);
        switch (clientId)
        {               
            case 2:
                result = await _userService.GetUser(context.UserName);
                break;
            case 3:
                //also using async/await correctly
                result = await _userService.Authenticate(context.UserName, context.Password);
                break;
            default:
                result = false;
                break;
        }

        if (result)
            context.AuthenticateResult = new AuthenticateResult(context.UserName, context.UserName);
   }

Update - After code posted

When using ASP.Net DI and IdentityServer DI together, we have to be careful to make sure that both the IdentityServer and the underlying DbContext are scoped to the OWIN request context, we do that by Injecting the DbContext into the IdentityServer context. this answer has some useful background: https://stackoverflow.com/a/42586456/1690217

I suspect all you need to do is resolve the DbContext, instead of explicitly instantiating it:

private void AddDependencyInjector(IdentityServerServiceFactory factory)
{     
      //here I inject all the services I need, as my DbContext       
      factory.Register(new Registration<ValueContext>(resolver => new ValueContext()));
      [...]

}

Supporting dicussion, largely irrelevant now...

With EF it is important to make sure that there are no concurrent queries against the same DbContext instance at the same time. Even though you have specified AsNoTracking() for this endpoint there is no indication that this endpoint is actually the culprit. The reason for synchronicity is so that the context can manage the original state, there are many internals that are simply not designed for multiple concurrent queries, including the way the database connection and transactions are managed.

(under the hood the DbContext will pool and re-use connections to the database if they are available, but ADO.Net does this for us, it happens at a lower level and so is NOT an argument for maintaining a singleton DbContext)

As a safety precaution, the context will actively block any attempts to re-query while an existing query is still pending.

EF implements the Unit-Of-Work pattern, you are only expected to maintain the same context for the current operation and should dispose of it when you are done. It can be perfectly acceptable to instantiate a DbContext scoped for a single method, you could instantiate multiple contexts if you so need them.

There is some anecdotal advice floating around the web based on previous versions of EF that suggest there is a heavy initialization sequence when you create the context and so they encourage the singleton use of the EF context. This advice worked in non-async environments like WinForms apps, but it was never good advice for entity framework.

When using EF in a HTTP based service architecture, the correct pattern is to create a new context for each HTTP request and not try to maintain the context or state between requests. You can manually do this in each method if you want to, however DI can help to minimise the plumbing code, just make sure that the HTTP request gets a new instance, and not a shared or recycled one.

Because most client-side programming can create multiple concurrent HTTP requests (this of a web site, how many concurrent requests might go to the same server for a single page load) it is a frivolous exercise to synchronise the incoming requests, or introduce a blocking pattern to ensure that the requests to the DbContext are synchronous or queued.

The overheads to creating a new context instance are expected to be minimal and the DbContext is expected to be used in this way especially for HTTP service implementations, so don't try to fight the EF runtime, work with it.

Repositories and EF

When you are using a repository pattern over the top of EF... (IMO an antipattern itself) it is important that each new instance of the repository gets its own unique instance of the DbContext . Your repo should function the same if you instead created the DbContext instance from scratch inside the Repo init logic. The only reason to pass in the context is to have DI or another common routine to pre-create the DbContext instance for you.

Once the DbContext instance is passed into the Repo, we lose the ability to maintain synchronicity of the queries against it, this is an easy pain point that should be avoided.
No amount of await or using synchronous methods on the DbContext will help you if multiple repos are trying to service requests at the same time against the same DbContext .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM