web-dev-qa-db-fra.com

Dependency Injection circular dependency .net core 2.0

Je souhaite que mon constructeur ApplicationContext ait la valeur UserManager comme paramètre, mais rencontre des problèmes avec l'injection de dépendance.

Code:

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

Et dans 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();

}

Code d'erreur:

Une dépendance circulaire a été détectée pour le service de type 'Microsoft.AspNetCore.Identity.UserManager`1 [RCI.App.Models.ApplicationUser]'.

Quelqu'un pourrait-il indiquer ce que je fais mal?

6
rory

Les dépendances circulaires sont généralement le signe d’une conception d’application inappropriée, qui doit être révisée. Comme je l'ai déjà mentionné dans les commentaires, avoir un contexte database qui dépend du gestionnaire d'utilisateurs ne semble pas être une bonne idée. Cela me fait supposer que le contexte de votre base de données fait trop et viole probablement le principe de responsabilité unique .

En regardant simplement les dépendances du contexte de votre base de données, vous ajoutez déjà trop d’états spécifiques à une application: vous avez non seulement une dépendance sur le gestionnaire d’utilisateurs, mais également sur l’accesseur de contexte HTTP; et vous résolvez également le contexte HTTP immédiatement dans le constructeur (ce qui n’est généralement pas la meilleure idée).

D'après votre extrait de code, il semble que vous souhaitiez récupérer l'utilisateur actuel pour une utilisation ultérieure. Si vous souhaitez utiliser ceci par exemple pour filtrer les requêtes de l'utilisateur, vous devez alors déterminer s'il est vraiment judicieux de l'intégrer de manière statique dans l'instance de contexte de base de données. Envisagez plutôt d’accepter une ApplicationUserinside méthodes. De cette façon, vous vous débarrassez de toutes ces dépendances, vous rendez le contexte de votre base de données plus contrôlable (puisque l'utilisateur n'est plus un state du contexte), et vous clarifiez également la responsabilité du contexte:

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

Notez qu'il s'agit de aussi _ ​​ inversion du contrôle . Au lieu que le contexte de base de données activement récupère l'utilisateur pour lequel il doit effectuer une requête (ce qui ajouterait une autre responsabilité, enfreignant le SRP), il s'attend à ce que l'utilisateur pour lequel il doit effectuer une requête soit transféré, déplaçant le contrôle vers le code appelant.

Désormais, si vous interrogez très souvent des éléments pour l'utilisateur actuel, il peut s'avérer fastidieux de résoudre l'utilisateur actuel dans un contrôleur, puis de le transmettre au contexte de la base de données. Dans ce cas, créez un service pour ne vous répétez plus . Ce service peut ensuite dépendre du contexte de la base de données et d'autres éléments pour déterminer l'utilisateur actuel.

Mais il ne suffit que de nettoyer le contexte de votre base de données de ce qu'il ne devrait pas faire pour réparer cette dépendance circulaire.

6
poke

Si vous n'avez pas réellement besoin de UserManager dans le constructeur, vous pouvez plutôt stocker une référence à IServiceProvider à la place:

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

Ensuite, lorsque vous aurez réellement besoin de la variable ApplicationUser, appelez par exemple. GetRequiredService<ApplicationUser>() (défini dans Microsoft.Extensions.DependencyInjection):

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

Bien sûr, vous pouvez utiliser un Lazy<T> pour charger par la première fois le gestionnaire ou l'utilisateur, puis y stocker une référence.

En règle générale, @poke a raison de refaire une architecture pour éviter de telles dépendances circulaires, mais laissez cette réponse ici si quelqu'un d'autre a un problème similaire et que le refactoring n'est pas une option.

8
Toby J

Merci beaucoup à Toby, pour la solution. Vous pouvez également utiliser Lazy<IMyService> pour éviter d'appeler _services.GetRequiredService<UserManager<ApplicationUser>>() chaque fois que vous voulez l'utiliser.

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

et quand vous voulez l'utiliser, dites simplement:

_userManager.value.doSomeThing();
1
محمد رضا