Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does the ASP.NET SPA template instantiate UserManager once for all requests?

I'm using the VS2013 ASP.NET SPA template as a starting point for my web application which uses the new ASP.NET identity framework. This is from the template:

public partial class Startup
 {
     static Startup()
     {
     UserManagerFactory = () => new UserManager<IdentityUser>(new UserStore<IdentityUser>());
....
}

So, since no DbContext is being passed into the UserStore constructor above, that indicates to me that a new DbContext is being created. Since I also wanted to make use of the data context (for other data operations during the request), I changed the template code slightly:

public partial class Startup
 {
public static DerivedDbContext=null;

    static Startup()
    {
   context = new DerivedDbContext();
           UserManagerFactory = () => new UserManager<IdentityUser>(new UserStore<IdentityUser>(context));
...
}

Now, I can use the same DbContext from any of my controllers by accessing:

Startup.context

But, I got into trouble when multiple requests would come in at the same time because unrelated operations were happening in the same DbContext. From here, it was pointed out to me that I should NOT be instantiating a single DbContext for the entire lifecycle of the application, but rather just for the lifecycle of a particular request, so I moved the instantiation to the controller's constructor, but now, I've got my own DbContext in the controller, and the UserManager still has its own (and is created in the Startup class).

Why does the template have the UserManager instantiated once for all users (in the Startup class)? Is it of concern that, by creating my own DbContext in a controller constructor, there are two DbContexts (the one I create in the controller's constructor, and the one in the UserManager, created in the Startup class) in play simultaneously? Is it acceptable to have one single UserManager shared across all requests, and yet unacceptable to share one DbContext across all requests in general?

Having two separate contexts in play seems foolish and I'm noticing that I sometimes get an out-of-sync view of data. I'd like to understand if anyone else faced this problem with this template.

**Edit: I understand that an IOC framework like nInject might help manage object lifecycles in this scenario, but I'd like to first gain an understanding of how to accomplish this without the help of such a framework.

Thanks... -Ben

like image 405
BenjiFB Avatar asked Oct 21 '22 18:10

BenjiFB


1 Answers

So the pattern should be one UserManager and one DbContext per request. In the 2.0-alpha1 release, we tried to make this easier by adding some new Identity Middleware that takes care of both of these things:

We are working on an updated samples package that demonstrates these, but in the meantime

You can add the following your IdentityModels.cs/Startup.Auth.cs with the new packages:

public class ApplicationUser : IdentityUser
{
    public async Task<ClaimsIdentity> GenerateUserIdentityAsync(UserManager<ApplicationUser> manager)
    {
        // Note the authenticationType must match the one defined in CookieAuthenticationOptions.AuthenticationType
        var userIdentity = await manager.CreateIdentityAsync(this, DefaultAuthenticationTypes.ApplicationCookie);
        // Add custom user claims here
        return userIdentity;
    }
}

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext()
        : base("DefaultConnection")
    {
    }

    public static ApplicationDbContext Create() {
        return new ApplicationDbContext();
    }
}

public class ApplicationUserManager : UserManager<ApplicationUser> {
    // Configure the application user manager
    public ApplicationUserManager(IUserStore<ApplicationUser> store) : base(store) {
    }

    public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options) {
        var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(options.Context.GetDbContext()));
        //var manager = new ApplicationUserManager(new AzureStore<ApplicationUser>());
        manager.UserValidator = new UserValidator<ApplicationUser>(manager) {
            AllowOnlyAlphanumericUserNames = false,
            RequireUniqueEmail = true
        };
        manager.PasswordValidator = new MinimumLengthValidator(6);
        var dataProtectionProvider = options.DataProtectionProvider;
        if (dataProtectionProvider != null) {
            manager.PasswordResetTokens = new DataProtectorTokenProvider(dataProtectionProvider.Create("PasswordReset"));
            manager.UserConfirmationTokens = new DataProtectorTokenProvider(dataProtectionProvider.Create("ConfirmUser"));
        }
        return manager;
    }
}

public static class OwinExtensions {
    public static IAppBuilder UseDbContextFactory(this IAppBuilder app, Func<DbContext> createCallback) {
        if (app == null) {
            throw new ArgumentNullException("app");
        }
        if (createCallback == null) {
            throw new ArgumentNullException("createCallback");
        }

        app.Use(typeof(IdentityFactoryMiddleware<DbContext, IdentityFactoryOptions<DbContext>>), 
            new IdentityFactoryOptions<DbContext>() { 
                Provider = new IdentityFactoryProvider<DbContext>() {
                    OnCreate = (options) => createCallback()
                }
            });
        return app;
    }

    public static DbContext GetDbContext(this IOwinContext context) {
        if (context == null) {
            throw new ArgumentNullException("context");
        }
        return context.Get<DbContext>();
    }
}

And the following goes into Startup.Auth.cs:

    public void ConfigureAuth(IAppBuilder app) {
        // Configure the db context and user manager to use per request
        app.UseDbContextFactory(ApplicationDbContext.Create);
        app.UseUserManagerFactory(new IdentityFactoryOptions<ApplicationUserManager>() {
            DataProtectionProvider = app.GetDataProtectionProvider(),
            Provider = new IdentityFactoryProvider<ApplicationUserManager>() {
                OnCreate = ApplicationUserManager.Create
            }
        });

        // Enable the application to use a cookie to store information for the signed in user
        // and to use a cookie to temporarily store information about a user logging in with a third party login provider
        // Configure the sign in cookie
        app.UseCookieAuthentication(new CookieAuthenticationOptions {
            AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
            LoginPath = new PathString("/Account/Login"),
            Provider = new CookieAuthenticationProvider {
                OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                    validateInterval: TimeSpan.FromSeconds(5),
                    regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
            }
        });
        app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
like image 74
Hao Kung Avatar answered Oct 28 '22 22:10

Hao Kung