.NET Core 中间件中的 ObjectDisposedException Entity Framework

ObjectDisposedException in .NET Core middleware with Entity Framework

我有一个用 .NET Core 编写的 API。我有一个令牌中间件,我遇到了一些问题。

如果我一次从一个客户端向中间件发送一个请求就可以了。如果我一次尝试由多个客户端使用中间件,我会出错。

据我所知,我认为 _userManager 并没有在每次发出请求时都创建一个新实例,我认为这导致了我的问题。我似乎得到

System.InvalidOperationException: An attempt was made to use the context while it is being configured

System.ObjectDisposedException: Cannot access a disposed object

我在想我正在注入的 UserManager<ApplicationUser> 可能是单例?也许这两个请求都试图使用相同的 _userManager 实例并且第一个请求处理它而第二个请求错误?

我对此有点陌生,所以我可能离题太远了。也许 _userManager 不应该以这种方式使用,或者我注入不正确。

这里是一些来自 starup.cs 和令牌中间件的代码。下面是一些错误日志。

startup.cs

的部分内容
public void ConfigureServices(IServiceCollection services)
{
     ...
     services.AddDbContext<ApplicationDbContext>(options =>
     {                                                                    


options.UseNpgsql(Environment.GetEnvironmentVariable("DbConnString"));                        
     });            


        services.AddIdentity<ApplicationUser, IdentityRole>(o=>
        {
            o.Password.RequireDigit = false;
            o.Password.RequiredLength = 0;
            o.Password.RequireLowercase = false;
            o.Password.RequireNonAlphanumeric = false;
            o.Password.RequireUppercase = false;
            o.SignIn.RequireConfirmedEmail = true;
            o.User.RequireUniqueEmail = true;

        })

                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();
        services.AddAuthentication(o =>
        {
            o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;

        }).AddJwtBearer(options =>
        {
            options.TokenValidationParameters = tokenValidationParameters;
        });
...
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
     ...
      app.UseJWTTokenProviderMiddleware(Options.Create(jwtOptions));            

        app.UseAuthentication();
        app.UseMvc();
     ...
 }

TokenProviderMiddleware

public class TokenProviderMiddleware
{           
    private readonly RequestDelegate _next;
    private TokenProviderOptions _options;
    private UserManager<ApplicationUser> _userManager;
    private readonly ILogger<TokenProviderMiddleware> _logger;
    public TokenProviderMiddleware(RequestDelegate next, IOptions<TokenProviderOptions> options, ILogger<TokenProviderMiddleware> logger)
    {
        _next = next;
        _options = options.Value;
        _logger = logger;            
    }

    public async Task Invoke(HttpContext context, UserManager<ApplicationUser> userManager)
    {
        _userManager = userManager;


        if (!context.Request.Path.Equals(_options.Path, StringComparison.Ordinal))
        {
            await _next(context);
            return;
        }
        if (!context.Request.Method.Equals("POST") || !context.Request.HasFormContentType)
        {
            context.Response.StatusCode = 400;
            await context.Response.WriteAsync("Bad Request");
            return;
        }

        ApplicationUser user = null;
        string refreshToken;
        string accessToken;

        if (context.Request.Form["grant_type"].Equals("refresh_token"))
        {
            refreshToken = context.Request.Form["refresh_token"];
            if(String.IsNullOrEmpty(refreshToken))
            {
                context.Response.StatusCode = 400;
                await context.Response.WriteAsync("refresh_token not sent");   
                return;
            }


            // Check that the refresh_token is valid
            // I don't like using exceptions for flow control but it's how ValidateToken works
            try
            {
                ValidateRefreshToken(refreshToken);
            }
            catch(SecurityTokenInvalidSignatureException ex)
            {
                context.Response.StatusCode = 400;
                await context.Response.WriteAsync("refresh_token failed validation");    
                return;                
            }
            catch(Exception ex)
            {
                context.Response.StatusCode = 400;
                await context.Response.WriteAsync("refresh_token failed validation");
                return;
            }
            user = await GetUserAsync(context, "from_refresh_token");
            accessToken = await GenerateAccessToken(user);

            var response = new
            {
                access_token = accessToken
            };
            context.Response.ContentType = "application/json";
            await context.Response.WriteAsync(JsonConvert.SerializeObject(response, new JsonSerializerSettings { Formatting = Formatting.Indented }));
            return;
        }



private async Task<ApplicationUser> GetUserAsync(HttpContext context, string getUserFrom)
    {
        ApplicationUser user = null;
        if(getUserFrom.Equals("from_username_password"))
        {
            string username = context.Request.Form["username"];
            string password = context.Request.Form["password"];

            if(String.IsNullOrEmpty(username) || String.IsNullOrEmpty(password))
            {
                throw new ArgumentException("Username/password not sent");
            }

            user = await _userManager.FindByNameAsync(username);
            var result = _userManager.CheckPasswordAsync(user, password);
            if (result.Result == false)
            {
                context.Response.StatusCode = 400;
                await context.Response.WriteAsync("Invalid username or password");                
            }
            if(!await _userManager.IsEmailConfirmedAsync(user))
            {
                context.Response.StatusCode = 400;
                await context.Response.WriteAsync("Email address not confirmed");
            }

        }

        else if (getUserFrom.Equals("from_refresh_token"))
        {
            JwtSecurityToken decodedRefreshToken = new JwtSecurityTokenHandler().ReadJwtToken(context.Request.Form["refresh_token"]);
            string uid = decodedRefreshToken.Payload.Claims.FirstOrDefault(x => x.Type == "uid").Value;
            user = await _userManager.FindByIdAsync(uid);
        }

        return user;
    }

错误日志

2017-12-02 19:45:27.171 -05:00 [Information] Request starting HTTP/1.1 POST http://0.0.0.0:5000/api/token application/x-www-form-urlencoded 204
2017-12-02 19:45:27.171 -05:00 [Information] Request starting HTTP/1.1 POST http://0.0.0.0:5000/api/token application/x-www-form-urlencoded 204
2017-12-02 19:45:27.171 -05:00 [Debug] "POST" requests are not supported
2017-12-02 19:45:27.171 -05:00 [Debug] "POST" requests are not supported
2017-12-02 19:45:27.171 -05:00 [Debug] The request has an origin header: '"http://0.0.0.0:8080"'.
2017-12-02 19:45:27.171 -05:00 [Debug] The request has an origin header: '"http://0.0.0.0:8080"'.
2017-12-02 19:45:27.171 -05:00 [Information] Policy execution successful.
2017-12-02 19:45:27.172 -05:00 [Information] Policy execution successful.
2017-12-02 19:45:27.172 -05:00 [Debug] Connection id ""0HL9PS1UOQ2DS"", Request id ""0HL9PS1UOQ2DS:000000C7"": started reading request body.
2017-12-02 19:45:27.172 -05:00 [Debug] Connection id ""0HL9PS1UOQ2E0"", Request id ""0HL9PS1UOQ2E0:0000000B"": started reading request body.
2017-12-02 19:45:27.175 -05:00 [Debug] Connection id ""0HL9PS1UOQ2DS"", Request id ""0HL9PS1UOQ2DS:000000C7"": done reading request body.
2017-12-02 19:45:27.175 -05:00 [Debug] Connection id ""0HL9PS1UOQ2E0"", Request id ""0HL9PS1UOQ2E0:0000000B"": done reading request body.
2017-12-02 19:45:27.450 -05:00 [Information] Entity Framework Core "2.0.0-rtm-26452" initialized '"ApplicationDbContext"' using provider '"Npgsql.EntityFrameworkCore.PostgreSQL"' with options: "None"
2017-12-02 19:45:27.762 -05:00 [Debug] System.InvalidOperationException occurred, checking if Entity Framework recorded this exception as resulting from a failed database operation.
2017-12-02 19:45:27.762 -05:00 [Debug] Entity Framework did not record any exceptions due to failed database operations. This means the current exception is not a failed Entity Framework database operation, or the current exception occurred from a DbContext that was not obtained from request services.
2017-12-02 19:45:27.763 -05:00 [Error] An unhandled exception has occurred while executing the request
System.InvalidOperationException: An attempt was made to use the context while it is being configured. A DbContext instance cannot be used inside OnConfiguring since it is still being configured at this point. This can happen if a second operation is started on this context before a previous operation completed. Any instance members are not guaranteed to be thread safe.
   at Microsoft.EntityFrameworkCore.DbContext.get_InternalServiceProvider()
   at Microsoft.EntityFrameworkCore.DbContext.get_DbContextDependencies()
   at Microsoft.EntityFrameworkCore.DbContext.Set[TEntity]()
   at Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore`9.get_UsersSet()
   at Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore`9.FindByIdAsync(String userId, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Identity.UserManager`1.FindByIdAsync(String userId)
   at MyProject.TokenProviderMiddleware.<GetUserAsync>d__9.MoveNext() in /home/jrow/Projects/MyProject/MyProject/Middleware/TokenProviderMiddleware.cs:line 220

System.ObjectDisposedException: Cannot access a disposed object. A common cause of this error is disposing a context that was resolved from dependency injection and then later trying to use the same context instance elsewhere in your application. This may occur if you are calling Dispose() on the context, or wrapping the context in a using statement. If you are using dependency injection, you should let the dependency injection container take care of disposing context instances.
Object name: 'AsyncDisposer'.
   at Microsoft.EntityFrameworkCore.Internal.ConcurrencyDetector.AsyncDisposer.Dispose()
   at Microsoft.EntityFrameworkCore.Query.Internal.AsyncLinqOperatorProvider.ExceptionInterceptor`1.EnumeratorExceptionInterceptor.<MoveNext>d__5.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
   at System.Linq.AsyncEnumerable.<Aggregate_>d__6`3.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore`9.<GetClaimsAsync>d__38.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Microsoft.AspNetCore.Identity.UserManager`1.<GetClaimsAsync>d__103.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at MyProject.TokenProviderMiddleware.<CreateClaims>d__10.MoveNext() in /home/jrow/Projects/MyProject/MyProject/Middleware/TokenProviderMiddleware.cs:line 239

如果您的 UserManager 依赖于 dbcontext,则它不应是单例。应该为每个请求单独创建 dbcontext。使您的 UserManager 成为临时的以正确管理 dbcontext。

通过传递变量而不是每次全局更新它来修复它。 (从 reddit 得到了答案)。

public async Task Invoke(HttpContext context, UserManager<ApplicationUser> userManager)
{

    ...
    try
    {
        user = await GetUserAsync(_userManager, context, "from_refresh_token");
    }
    ...
}

private async Task<ApplicationUser> GetUserAsync(UserManager<ApplicationUser> _userManager, HttpContext context, string getUserFrom)
{
    //Do stuff with _userManager
}