Jrow
Jrow

Reputation: 1054

ObjectDisposedException in .NET Core middleware with Entity Framework

I have an API written in .NET Core. I have a token middleware that I'm having some issues with.

If I send a request to the middleware from one client at a time it's fine. If I try to use the middleware by more than one client at a time I get errors.

From what I can tell I think the _userManager doesn't have a new instance created every time a request is made and I think this causes my problem. I seem to either get

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

or

System.ObjectDisposedException: Cannot access a disposed object

I'm thinking maybe the UserManager<ApplicationUser> that I'm injecting is a singleton? Maybe both requests are trying to use the same instance of _userManager and the first one disposes it and the second request errors?

I'm sorta new to this so maybe I'm way off base. Maybe _userManager wasn't meant to be used this way or maybe I'm injecting it incorrectly.

Here is some code from the starup.cs and token middleware. Below that are some of the error logs.

Parts of 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;
    }

Error logs

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

Upvotes: 0

Views: 4416

Answers (2)

Jrow
Jrow

Reputation: 1054

Fixed it by passing the variable around instead of globally newing it every time. (got an answer from 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
}

Upvotes: 1

Peter
Peter

Reputation: 27944

If your UserManager is depending on a dbcontext, it should not be a singleton. The dbcontext should be created separately for each request. Make your UserManager transient to manage the dbcontext correctly.

Upvotes: 0

Related Questions