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

24
我希望我的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();

}

错误消息:

检测到类型为“Microsoft.AspNetCore.Identity.UserManager`1 [RCI.App.Models.ApplicationUser]”的服务存在循环依赖关系。

能否有人指出我做错了什么?


3
你提供的示例中,UserManager<ApplicationUser>ApplicationContext具有明确的依赖关系,导致循环依赖。当解析ApplicationContext时,它必须创建一个需要ApplicationContextUserManager<ApplicationUser>。你能看出这会导致什么问题吗? - Nkosi
2
数据库上下文中依赖于用户管理器似乎不是一个好主意。您可能应该使用一个服务,该服务依赖于数据库上下文和用户管理器。 - poke
1
@rory,您已经可以在应用程序上下文中访问“Users”。直接使用当前请求的用户ID在其中进行查询即可,无需引用用户管理器。 - Nkosi
2
UserManager<T> 依赖于 UserStore<T>,而 UserStore<T> 又依赖于在 ASP.NET Core Identity 中注册的数据库上下文,这恰好是您的数据库上下文,它又依赖于用户管理器。 - poke
2
@rory,你还应该保持ApplicationContext构造函数的简洁性,并且不要在其中尝试访问用户或进行查询。在目标方法中提取用户并进行查询,因为此时请求已经完全实现。它基本上只需要_contextAccessor = contextAccessor;,其余操作应该在CRUD操作之一完成。 - Nkosi
显示剩余6条评论
3个回答

30

如果构造函数中实际上不需要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在重新设计以避免这种循环依赖方面是正确的,但是我将保留此答案,以防其他人有类似的问题,而重构不是一个选项。


24
循环依赖通常是不正确的应用程序设计的迹象,应该进行修改。正如我在评论中已经提到的那样,具有依赖于用户管理器的数据库上下文似乎不是一个好主意。这让我认为你的数据库上下文做了太多的事情,很可能违反了单一职责原则
仅仅看一下你的数据库上下文的依赖关系,你就已经在其中添加了太多的应用程序特定状态:你不仅依赖于用户管理器,还依赖于HTTP上下文访问器;并且你还立即在构造函数中解析了HTTP上下文(这通常不是最好的想法)。
从你的代码片段中可以看出,你想要检索当前用户以供以后使用。如果你想将其用于例如过滤用户的查询,那么你应该考虑是否真的把它静态地嵌入到数据库上下文实例中是一个好主意。考虑在方法内部接受一个ApplicationUser。这样,你就可以摆脱所有这些依赖关系,使你的数据库上下文更易于测试(因为用户不再是上下文的状态),并且也使上下文的单一职责更加清晰:
public IList<Thing> GetThings (ApplicationUser user)
{
    // just an example…
    return Things.Where(t => t.UserId == user.Id).ToList();
}

请注意,这也是控制反转。不应该让数据库上下文主动检索它应该查询的用户(这将增加另一个责任,违反SRP),而是期望传递应该查询的用户,将控制权移交给调用代码。
现在,如果您经常为当前用户查询内容,那么在控制器中解析当前用户然后将其传递给数据库上下文可能会变得有些繁琐。在这种情况下,创建一个服务来不再重复自己。该服务可以依赖于数据库上下文和其他东西来确定当前用户。
但仅仅清除数据库上下文不应该做的事情就足以解决这个循环依赖关系问题。

11
非常感谢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();

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