How to inject UserManager & SignInManager

2019-01-10 20:02发布

问题:

I am trying to figure out how to inject UserManager and SignInManager. I have installed Ninject in my application and I am using it in the following manner:

Please consider this to be a brand new project. Inside Startup.cs I have the following:

public partial class Startup
{
    public void Configuration(IAppBuilder app)
    {
        ConfigureAuth(app);

        app.UseNinjectMiddleware(CreateKernel);
    }

    private static IKernel CreateKernel()
    {
        var kernel = new StandardKernel();
        kernel.Load(Assembly.GetExecutingAssembly());


        return kernel;
    }
}

now if I were to create some Dummy class and try to inject it based on its interface that works. I have tested it. What I am trying to figure out is how would I now strip out the following out of Startup.Auth.cs and inject it. Having no interfaces I can rely on, I am not sure how this is done:

app.CreatePerOwinContext(ApplicationDbContext.Create);
app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);

Just to clarify one more time, my question is: How do I instantiate ApplicationUserManager and ApplicationSignInManager and inject it in my controller parameters. Here is the controller that I am trying to inject this into:

public AccountController(ApplicationUserManager userManager, ApplicationSignInManager signInManager)
{
    UserManager = userManager;
    SignInManager = signInManager;
}

EDIT:

Here is what I tried:

private static IKernel CreateKernel()
{
    var kernel = new StandardKernel();
    kernel.Load(Assembly.GetExecutingAssembly());

    kernel.Bind<IUserStore<ApplicationUser>>().To<UserStore<ApplicationUser>>();
    kernel.Bind<UserManager<ApplicationUser>>().ToSelf();

    return kernel;
}

But with this I get null reference error

回答1:

Prerequisites

Start a new MVC5 application using the MVC template. This will install all the necessary dependencies as well as deploy the Startup.Auth.cs file which contains the bootstrap code for Microsoft.AspNet.Identity (it als includes the all the references for Microsoft.AspNet.Identity).

Install the following packages and update them to the latest afterwards.

Install-Package Ninject
Install-Package Ninject.MVC5

Configuration

Remove the default constructor on the AccountController so only the parameterized constructor remains. It should have the follownig signature.

public AccountController(ApplicationUserManager userManager, ApplicationSignInManager signInManager)

This will ensure that you will get an error if injection fails which is what we want.

NInject Configuration

The NInject NuGet package deployment will have created a file named NinjectWebCommon.cs where the boiler plate NInject registration takes place. This has a method with the following signature which you can extend with your registrations.

private static void RegisterServices(IKernel kernel)

Now we will add the following code to this method to get NInject automatically inject the ApplicationSignInManager and ApplicationUserManager instances.

private static void RegisterServices(IKernel kernel) {
    kernel.Bind<IUserStore<ApplicationUser>>().To<UserStore<ApplicationUser>>();
    kernel.Bind<UserManager<ApplicationUser>>().ToSelf();

    kernel.Bind<HttpContextBase>().ToMethod(ctx => new HttpContextWrapper(HttpContext.Current)).InTransientScope();

    kernel.Bind<ApplicationSignInManager>().ToMethod((context)=>
    {
        var cbase = new HttpContextWrapper(HttpContext.Current);
        return cbase.GetOwinContext().Get<ApplicationSignInManager>();
    });

    kernel.Bind<ApplicationUserManager>().ToSelf();
}

Thats it. Now you should be able to navigate to the Login or Register links and injection will occur.

Alternate Approach

I prefer a Proxy approach that exposes limited functionality for the ApplicationSignInManager and the ApplicationUserManager instances. I then inject this proxy into the necessary controllers. It helps abstract some of the Identity information away from the controllers which makes it easier to change in the future. This is not a new concept by any means and whether you do this or not depends on the size and complexity of your project as well as how you want to handle dependencies. So the benefits are (common actually for any proxy):

  • You can abstract some of the dependencies away from your code
  • You can streamline some of the calls within the api
  • You can expose only that functionality that you want to use including configurable parts
  • Change management should be easier if the interfaces ever change, now you change the calls in your proxy instead of all the calling code across your controllers.

Code example:

public interface IAuthManager
{
    Task<SignInStatus> PasswordSignInAsync(string userName, string password, bool rememberMe);
}

public class AuthManager : IAuthManager
{
    private ApplicationUserManager _userManager;
    ApplicationSignInManager _signInManager;

    public AuthManager(ApplicationUserManager userManager, ApplicationSignInManager signInManager)
    {
        this._userManager = userManager;
        this._signInManager = signInManager;
    }

    public Task<SignInStatus> PasswordSignInAsync(string userName, string password, bool rememberMe)
    {
        return _signInManager.PasswordSignInAsync(userName, password, rememberMe, true);
    }
}

Add the following line in your NInject dependency registration.

kernel.Bind<IAuthManager>().To<AuthManager>();

Alter your AccountController constructor to take in an instance of IAuthManager. Finally change your methods to refer to this proxy instead of the ASP.NET Identity classes directly.

Disclaimer - I did not wire up a complex call, just a very simple one to illustrate my point. This is also entirely optional and whether you do it or not really should depend on the scope and size of your project and how you plan on using the ASP.NET Identity framework



回答2:

To give an exact answer to what my question stated, here is the code and instructions:

Step 1: Create custom User Store

public class ApplicationUserStore : UserStore<ApplicationUser>
{
    public ApplicationUserStore(ApplicationDbContext context)
        : base(context)
    {
    }
}

Step 2: Update ApplicationUserManager and move code from Create method into constructor

public class ApplicationUserManager : UserManager<ApplicationUser>
{
    public ApplicationUserManager(IUserStore<ApplicationUser> store, IdentityFactoryOptions<ApplicationUserManager> options)
        : base(store)
    {
        this.UserValidator = new UserValidator<ApplicationUser>(this)
        {
            AllowOnlyAlphanumericUserNames = false,
            RequireUniqueEmail = true
        };

        // Configure validation logic for passwords
        this.PasswordValidator = new PasswordValidator
        {
            RequiredLength = 6,
            RequireNonLetterOrDigit = true,
            RequireDigit = true,
            RequireLowercase = true,
            RequireUppercase = true,
        };

        // Configure user lockout defaults
        this.UserLockoutEnabledByDefault = true;
        this.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);
        this.MaxFailedAccessAttemptsBeforeLockout = 5;

        // Register two-factor authentication providers. This application uses Phone and Emails as a step of receiving a code for verifying the user
        // You can write your own provider and plug it in here.
        this.RegisterTwoFactorProvider("Phone Code", new PhoneNumberTokenProvider<ApplicationUser>
        {
            MessageFormat = "Your security code is {0}"
        });
        this.RegisterTwoFactorProvider("Email Code", new EmailTokenProvider<ApplicationUser>
        {
            Subject = "Security Code",
            BodyFormat = "Your security code is {0}"
        });
        this.EmailService = new EmailService();
        this.SmsService = new SmsService();
        var dataProtectionProvider = options.DataProtectionProvider;
        if (dataProtectionProvider != null)
        {
            this.UserTokenProvider =
                new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
        }
    }
}

Step 3: Modify the Startup.Auth class and comment out the following code

//app.CreatePerOwinContext(ApplicationDbContext.Create);
//app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
//app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);

Step 4: Update Account Controller (or any other controller in question) and add the following constructor

public AccountController(ApplicationUserManager userManager, ApplicationSignInManager signInManager, IAuthenticationManager authManager)
{
    _userManager = userManager;
    _signInManager = signInManager;
    _authManager = authManager;
}

Step 5: Update Account Controller and make properties only retrivable as so:

public ApplicationSignInManager SignInManager
{
    get
    {
        return _signInManager;
    }
}

public ApplicationUserManager UserManager
{
    get
    {
        return _userManager;
    }
}

private IAuthenticationManager AuthenticationManager
{
    get
    {
        return _authManager;
    }
}

Step 6: Update Startup.cs

public partial class Startup
{
    private IAppBuilder _app;
    public void Configuration(IAppBuilder app)
    {
        ConfigureAuth(app);
        _app = app;
        app.UseNinjectMiddleware(CreateKernel);
    }

    private IKernel CreateKernel()
    {
        var kernel = new StandardKernel();
        kernel.Load(Assembly.GetExecutingAssembly());

        kernel.Bind<ApplicationDbContext>().ToSelf().InRequestScope();
        kernel.Bind<IUserStore<ApplicationUser>>().To<ApplicationUserStore>();
        kernel.Bind<ApplicationUserManager>().ToSelf();
        kernel.Bind<ApplicationSignInManager>().ToSelf();
        kernel.Bind<IAuthenticationManager>().ToMethod(x => HttpContext.Current.GetOwinContext().Authentication);
        kernel.Bind<IDataProtectionProvider>().ToMethod(x => _app.GetDataProtectionProvider());

        return kernel;
    }
}

To further expand the answer to this question, based on the comments I have received:

These managers should not be injected as classes as then you are not accomplishing DI. What should be done instead is create multiple interfaces that further separate and group methods of UserManager according to your needs. Here is an example:

public interface IUserManagerSegment
{
    Task<IdentityResult> CreateAsync(ApplicationUser user, string password);
    Task<IdentityResult> CreateAsync(ApplicationUser user);
    Task<IdentityResult> ConfirmEmailAsync(string userId, string token);
    Task<ApplicationUser> FindByNameAsync(string userName);
    Task<bool> IsEmailConfirmedAsync(string userId);
    Task<IdentityResult> ResetPasswordAsync(string userId, string token, string newPassword);
    Task<IList<string>> GetValidTwoFactorProvidersAsync(string userId);
    Task<IdentityResult> AddLoginAsync(string userId, UserLoginInfo login);
    void Dispose(bool disposing);
    void Dispose();
}

The above method has a list of few random methods I chose just to illustrate the point. With this said, we would now inject the method based on the interface such as this:

kernel.Bind<IUserManagerSegment>().To<ApplicationUserManager>();

And now our AccountController constructor would look like this:

public AccountController(IUserManagerSegment userManager, ApplicationSignInManager signInManager, IAuthenticationManager authManager)  
{
    _userManager = userManager;
    _signInManager = signInManager;
    _authManager = authManager;
}

Same thing should be done to SignInManager and AuthenticationManager.

The code above has been tested and is working. Just ensure you have referenced the following DLLs:

Ninject.dll
Ninject.Web.Common
Ninject.Web.Common.OwinHost
Ninject.Web.Mvc