依赖注入循环依赖.NET Core 2.0

Dependency Injection circular dependency .NET Core 2.0

我希望我的 ApplicationContext 构造函数将 UserManager 作为参数,但我在依赖注入方面遇到了问题。

代码:

public class ApplicationContext : IdentityDbContext<ApplicationUser>
{
    private IHttpContextAccessor _contextAccessor { get; set; }
    public ApplicationUser ApplicationUser { get; set; }
    private UserManager<ApplicationUser> _userManager;

    public ApplicationContext(DbContextOptions<ApplicationContext> options, IHttpContextAccessor contextAccessor, UserManager<ApplicationUser> userManager)
        : base(options)
    {
        _contextAccessor = contextAccessor;
        var user = _contextAccessor.HttpContext.User;
        _userManager = userManager;
        ApplicationUser = _userManager.Users.FirstOrDefault(u => u.Id == _userManager.GetUserId(user));
    }
}

并且在 startup.cs 中:

public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddDbContext<ApplicationContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"), b => b.MigrationsAssembly("RCI.App")));

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<ApplicationContext>()
        .AddDefaultTokenProviders();

    services.AddAuthentication();

    services.AddMvc();

    // Add application services.
    services.AddTransient<IEmailSender, AuthMessageSender>();
    services.AddTransient<ISmsSender, AuthMessageSender>();
    services.AddTransient<IHttpContextAccessor, HttpContextAccessor>();

    services.AddOptions();

}

错误信息:

A circular dependency was detected for the service of type 'Microsoft.AspNetCore.Identity.UserManager`1[RCI.App.Models.ApplicationUser]'.

谁能指出我做错了什么?

循环依赖通常是应用程序设计不当的标志,应予以修改。正如我在评论中已经提到的,拥有一个依赖于用户管理器的 database 上下文似乎不是一个好主意。这让我假设您的数据库上下文 太多 并且可能违反了 single-responsibility principle.

仅查看数据库上下文的依赖关系,您已经在其中添加了太多应用程序特定状态:您不仅依赖于用户管理器,还依赖于 HTTP 上下文访问器;并且您也在构造函数中立即解析 HTTP 上下文(这通常不是最好的主意)。

从您的代码摘录来看,您似乎想要检索当前用户供以后使用。例如,如果你想使用它来过滤用户的查询,那么你应该考虑将它静态地烘焙到数据库上下文实例中是否真的是个好主意。考虑改为接受 ApplicationUser 内部方法 。这样,您就摆脱了所有这些依赖性,使您的数据库上下文更好地可测试(因为用户不再是上下文的 state),并且您还承担了单一责任上下文更清晰:

public IList<Thing> GetThings (ApplicationUser user)
{
    // just an example…
    return Things.Where(t => t.UserId == user.Id).ToList();
}

注意这是也是inversion of control。它不是让数据库上下文 主动 检索它应该查询的用户(这会增加另一个责任,违反 SRP),而是希望传递它应该查询的用户,移动控制调用代码。

现在,如果您经常查询当前用户的内容,在控制器中解析当前用户然后将其传递给数据库上下文可能会变得有些烦人。在这种情况下,为 no longer repeat yourself 创建一个服务。该服务然后可以依赖于数据库上下文和其他东西来确定当前用户。

但是,只需从数据库上下文中清除它 不应该 做的事情就足以修复这种循环依赖。

如果您实际上不需要构造函数中的 UserManager,您可以存储对 IServiceProvider 的引用:

private IHttpContextAccessor _contextAccessor { get; set; }
public ApplicationUser ApplicationUser { get; set; }
private IServiceProvider _services;

public ApplicationContext(DbContextOptions<ApplicationContext> options,
    IHttpContextAccessor contextAccessor, IServiceProvider services)
    : base(options)
{
    _contextAccessor = contextAccessor;
    var user = _contextAccessor.HttpContext.User;
    _services = services;
}

然后,当您确实需要 ApplicationUser 时,例如调用GetRequiredService<ApplicationUser>()(在Microsoft.Extensions.DependencyInjection中定义):

var manager = _services.GetRequiredService<UserManager<ApplicationUser>>();
var user = manager.Users.FirstOrDefault(u => u.Id == _userManager.GetUserId(user));

当然,您可以使用 Lazy<T> 来延迟加载管理器或用户,然后存储对它的引用。

总的来说,@poke 关于重新架构以避免此类循环依赖的说法是正确的,但将此答案留在此处以防其他人遇到类似问题并且重构不是一种选择。

非常感谢 Toby 的解决方案。您还可以使用 Lazy<IMyService> 来防止每次您想使用它时调用 _services.GetRequiredService<UserManager<ApplicationUser>>()

private IHttpContextAccessor _contextAccessor { get; set; }
public ApplicationUser ApplicationUser { get; set; }
private Lazy<UserManager<ApplicationUser>> _userManager;

public ApplicationContext(DbContextOptions<ApplicationContext> options,
    IHttpContextAccessor contextAccessor, IServiceProvider services)
    : base(options)
{
    _contextAccessor = contextAccessor;
    var user = _contextAccessor.HttpContext.User;
    _userManager = new Lazy<UserManager<ApplicationUser>>(() =>
                services.GetRequiredService<UserManager<ApplicationUser>>());
}

当你想使用它时,只需说:

_userManager.value.doSomeThing();