如何注入UserManager和SignInManager

19

我正试图弄清楚如何注入UserManager和SignInManager。我已经在我的应用程序中安装了Ninject,并以以下方式使用它:

请将此视为全新的项目。在Startup.cs中,我有以下内容:

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

现在,如果我要创建一些虚拟类,并尝试基于其接口进行注入,那是可以的。我已经测试过了。我现在想弄清楚的是如何从Startup.Auth.cs中剥离以下内容并进行注入。由于没有可依赖的接口,我不确定该怎么做:

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

再次澄清一下,我的问题是:如何实例化ApplicationUserManager和ApplicationSignInManager并将其注入到我的控制器参数中。以下是我正在尝试将其注入的控制器:

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

编辑:

以下是我尝试过的内容:

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

但是这样会导致我得到空引用错误


你尝试过 kernel.Bind<ApplicationUserManager>().ToMethod(ctx => ApplicationUserManager.Create()).InRequestScope()(等等)吗? - BatteryBackupUnit
2
这里有关于配置MVC5模板用于依赖注入的教程,这里这里。我认为后者更好,尽管它忽略了对“ManageController”所需更改的说明。一旦您将库存类和控制器重构为使用构造函数注入,就可以轻松地更换其他DI容器。 - NightOwl888
2
这里有一个Gist,其中包含了对项目的更改(暂时忽略Startup.cs的更改)。我建议在Ninject.Web.Common上开一个问题,以获取说明,因为文档非常不清楚你应该如何进行OWIN MVC 5集成。我试图解决它,但Ninject正在以一种奇怪的方式做事情,需要一个引导程序,而OWIN已经有一个(并传递其配置)。另一个选择是-您可以随时更改为其他容器。 - NightOwl888
MVC5和Web.API使用两个不同的堆栈(不同的请求生命周期、DI容器等)。只有Web.API基于OWIN。因此,当您使用UseNinjectMiddleware时,它只能与ApiControllers一起使用。如果您想使其与常规控制器一起工作,则需要使用NinjectWebCommon类的RegisterServices方法,该方法将在使用Ninject.MVC5包的情况下添加到您的项目中。 - nemesv
@Igor,这里有一个Autofac指南:https://www.talksharp.com/configuring-autofac-to-work-with-the-aspnet-identity-framework-in-mvc-5,但我正在尝试将其翻译为Ninject,因为Ninject不像Autofac那样使用容器构建器,这让我感到困惑。不过还是感谢你的提供! - Bagzli
显示剩余15条评论
2个回答

24

前提条件

使用MVC模板启动一个新的MVC5应用程序。这将安装所有必要的依赖项,并部署包含Microsoft.AspNet.Identity引导代码的Startup.Auth.cs文件(它还包括所有Microsoft.AspNet.Identity的引用)。

安装以下软件包并随后更新到最新版本。

Install-Package Ninject
Install-Package Ninject.MVC5

配置

移除AccountController上的默认构造函数,只保留带参数的构造函数,其签名应为以下内容:

public AccountController(ApplicationUserManager userManager, ApplicationSignInManager signInManager)

这将确保如果注入失败,您将收到一个错误,这正是我们想要的。

NInject 配置

NInject NuGet包部署将创建一个名为NinjectWebCommon.cs的文件,其中包含样板NInject注册。该文件有一个方法,其签名如下,您可以通过添加您的注册信息来扩展它。

private static void RegisterServices(IKernel kernel)

现在,我们将向该方法添加以下代码,以使NInject自动注入ApplicationSignInManagerApplicationUserManager实例。

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();
}

这就是全部内容。现在你应该能够导航到“登录”或“注册”链接,注入就会发生。

替代方法

我更倾向于使用代理方法,它为ApplicationSignInManagerApplicationUserManager实例公开了有限的功能。然后将此代理注入到必要的控制器中。它有助于将一些身份验证信息从控制器中抽象出来,这样就更容易在将来进行更改。这并不是一个新概念,无论是否使用此方法取决于您的项目规模和复杂性以及您想如何处理依赖项。因此,它带来的好处共有以下几点(对于任何代理都是普遍的):

  • 您可以将某些依赖项抽象出来,使得代码更加通用。
  • 您可以简化api中的某些调用。
  • 您可以仅公开您想要使用的功能,包括可配置部分。
  • 如果接口有所更改,则更改管理应该更容易,现在您只需更改代理中的调用,而不是跨所有控制器的调用代码。

代码示例:

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);
    }
}
在您的 NInject 依赖项注册中添加以下行。
kernel.Bind<IAuthManager>().To<AuthManager>();

将您的AccountController构造函数更改为接受一个IAuthManager实例。最后,更改方法引用此代理而不是直接引用ASP.NET标识类。

免责声明-我没有连接复杂的调用,只是非常简单地说明了我的观点。这也完全是可选的,您是否这样做应该取决于您的项目的范围和大小以及您计划如何使用ASP.NET标识框架


1
另一种方法是最重要的部分。该服务应该用于创建某种仓库。注入这样的服务并没有比实例化它更有价值。你真的会用具有完全相同公开方法的另一个服务来替换它吗? - Dave Alperovich
1
@DaveAlperovich - 我不久前开始了一个项目,将所有ASP.NET Identity库抽象到自己的独立程序集(csproj)中,并通过接口公开了有限的功能。这个项目的范围和代码行数非常大,所以这是有意义的。我还有几个内部项目,规模和范围非常有限,只花了几天时间创建,这里我没有进行任何抽象,因为这并不值得花费时间或精力。虽然我更喜欢使用代理/外观,但我认为是否使用取决于ROI,这可能基于许多因素。 - Igor
1
我会更进一步。这不是一个受欢迎的观点,但我不相信在注入像UserManager这样的服务中有任何投资回报率。将它们包装、抽象化,并创建具有功能分区的存储库。遗憾的是,DI已经成为了一个目的而不是手段。 - Dave Alperovich
@Igor,根据你的示例,你是否需要修改UserManager.Create方法并将其移动到构造函数中,就像我所做的那样? - Bagzli
@Bagzli - 不需要。我所做的唯一更改就是我上面列出的那些。但如果你需要,我可以将我的示例项目压缩并放在某个地方供你下载。 - Igor
显示剩余2条评论

20
为了准确回答我的问题,这里是代码和说明:
步骤1: 创建自定义用户存储。
public class ApplicationUserStore : UserStore<ApplicationUser>
{
    public ApplicationUserStore(ApplicationDbContext context)
        : base(context)
    {
    }
}

第二步: 更新ApplicationUserManager并将代码从Create方法移动到构造函数中

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"));
        }
    }
}

第三步: 修改Startup.Auth类,并注释掉以下代码

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

第四步: 更新帐户控制器(或其他相关控制器),并添加以下构造函数

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

步骤5:更新账户控制器,使属性仅可检索为:

public ApplicationSignInManager SignInManager
{
    get
    {
        return _signInManager;
    }
}

public ApplicationUserManager UserManager
{
    get
    {
        return _userManager;
    }
}

private IAuthenticationManager AuthenticationManager
{
    get
    {
        return _authManager;
    }
}

步骤6:更新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;
    }
}

为了进一步回答这个问题,基于我所收到的评论:
这些管理器不应该被注入为类,因为这样你就无法完成 DI。相反,应该创建多个接口,根据需要进一步分离和组织 UserManager 的方法。以下是一个示例:
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();
}

以上方法是我选择的一些随机方法,只是为了说明这一点。 话虽如此,现在我们将根据接口注入方法,例如:

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

现在我们的AccountController构造函数看起来像这样:

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

SignInManager和AuthenticationManager也应该采取同样的措施。

以上代码已经过测试并且可以正常工作。请确保您已引用以下DLL文件:

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


2
很好,看到你发布这个问题的时候恰好我也在发布我的解决方案。我用VS 2015进行了全新的设置测试。如果你还有任何问题或需要查看其他内容,请告诉我。 - Igor
1
看一下@Igor的回答。说实话,虽然你在技术上已经做到了一些事情,但注入UserManager并不是重点。DI的目的是解耦,这样你就可以用另一个服务替换一个服务。但你会用另一个UserManager替换它吗?它必须暴露完全相同的方法。最好创建一个用户存储库,其中包含像findUser(userid)ChangePasswordUser(userid, newpswd)等方法。这种存储库值得注入...并且可以以有价值的方式进行替换。 - Dave Alperovich
1
是的。如果您注入了一个暴露UserManager的每个方法的接口,那么您并没有取得多大成就。如果您想替换服务,因为您已经转移到不同的身份验证提供者或新版本,则除非所有方法保持不变,否则您的代码仍将更改。这就是为什么您希望通过更简单的接口注入更多的块服务。为此,您需要使用执行较少操作的UserRepo来包装您的UserManager,并嵌套逻辑。 - Dave Alperovich
1
@DaveAlperovich - 同意。Bagzil - 我上面的接口示例非常薄弱,因为它更多地是设计用来说明如何注入它并使其实现仍然可以访问signinmanager和usermanager。您的实际实现应更接近Facade模式,其中您抽象出ASP.NET Identity Framework的细节,并公开自己的简化界面,其中隐藏了布线和详细信息。 - Igor
1
@Bagzli - 关于你的添加。这个想法是从你的控制器中隐藏UserManager和SignInManager,并且只通过你的新 “IUserManagerSegment” 访问功能。这将使测试和后来改变实现更加容易。另外,如果你想要一些可处置的东西,那么就实现“System.IDisposable”,这样你的签名就会是“public interface IUserManagerSegment:IDisposable”,然后从你的接口中删除Dispose方法。这样CLR就能认出它,如果你想的话,就可以使用using块。 - Igor
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接