web-dev-qa-db-fra.com

Comment dois-je accéder à mes propriétés ApplicationUser à partir des vues ASP.NET Core?

Je travaille sur un projet ASP.Net vNext/MVC6. Je me familiarise avec l'identité ASP.Net.

La classe ApplicationUser est apparemment là où je suis censé ajouter des propriétés utilisateur supplémentaires, et cela fonctionne avec Entity Framework et mes propriétés supplémentaires sont stockées dans la base de données comme prévu.

Cependant, le problème survient lorsque je souhaite accéder aux détails de l'utilisateur actuellement connecté à partir de mes vues. Plus précisément, j'ai un _loginPartial.cshtml Dans lequel je veux récupérer et afficher l'icône Gravatar de l'utilisateur, pour laquelle j'ai besoin de l'adresse e-mail.

La classe de base Razor View a une propriété User, qui est un ClaimsPrincipal. Comment revenir de cette propriété User à ma ApplicationUser, pour récupérer mes propriétés personnalisées?

Notez que je ne demande pas comment trouver les informations; Je sais comment rechercher un ApplicationUser à partir de la valeur de User.GetUserId(). Il s'agit plutôt de savoir comment aborder ce problème de manière sensée. Plus précisément, je ne veux pas:

  • Effectuer toute sorte de recherche de base de données depuis mes vues (séparation des préoccupations)
  • Doit ajouter une logique à chaque contrôleur pour récupérer les détails de l'utilisateur actuel (principe DRY)
  • Vous devez ajouter une propriété User à chaque ViewModel.

Cela semble être une `` préoccupation transversale '' qui devrait avoir une solution standard centralisée, mais j'ai l'impression qu'il me manque une pièce du puzzle. Quelle est la meilleure façon d'accéder à ces propriétés utilisateur personnalisées à partir des vues?

Remarque: Il semble que l'équipe MVC ait contourné ce problème dans les modèles de projet en s'assurant que la propriété UserName est toujours définie sur l'adresse e-mail de l'utilisateur, évitant soigneusement la nécessité pour eux d'effectuer cette recherche pour obtenir l'adresse e-mail de l'utilisateur! Cela semble être un peu une triche pour moi et dans ma solution, le nom de connexion de l'utilisateur peut ou non être son adresse e-mail, donc je ne peux pas compter sur cette astuce (et je soupçonne qu'il y aura d'autres propriétés auxquelles j'aurai besoin d'accéder plus tard ).

19
Tim Long

Mise à jour de la réponse d'origine: (Cela viole la première exigence de l'op, voir ma réponse d'origine si vous avez la même exigence) Vous pouvez le faire sans modifier les revendications et en ajoutant le fichier d'extension (dans ma solution d'origine) en référençant FullName dans la vue Razor comme:

@UserManager.GetUserAsync(User).Result.FullName

Réponse originale:

Ceci est à peu près juste un exemple plus court de cette question de stackoverflow et après cela tutoriel .

En supposant que la propriété soit déjà configurée dans "ApplicationUser.cs" ainsi que dans les ViewModels et les vues applicables pour l'enregistrement.

Exemple utilisant "FullName" comme propriété supplémentaire:

Modifiez la méthode d'enregistrement "AccountController.cs" pour:

    public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
        {
            ViewData["ReturnUrl"] = returnUrl;
            if (ModelState.IsValid)
            {
                var user = new ApplicationUser {
                    UserName = model.Email,
                    Email = model.Email,
                    FullName = model.FullName //<-ADDED PROPERTY HERE!!!
                };
                var result = await _userManager.CreateAsync(user, model.Password);
                if (result.Succeeded)
                {
                    //ADD CLAIM HERE!!!!
                    await _userManager.AddClaimAsync(user, new Claim("FullName", user.FullName)); 

                    await _signInManager.SignInAsync(user, isPersistent: false);
                    _logger.LogInformation(3, "User created a new account with password.");
                    return RedirectToLocal(returnUrl);
                }
                AddErrors(result);
            }

            return View(model);
        }

Et puis j'ai ajouté un nouveau fichier "Extensions/ClaimsPrincipalExtension.cs"

using System.Linq;
using System.Security.Claims;
namespace MyProject.Extensions
    {
        public static class ClaimsPrincipalExtension
        {
            public static string GetFullName(this ClaimsPrincipal principal)
            {
                var fullName = principal.Claims.FirstOrDefault(c => c.Type == "FullName");
                return fullName?.Value;
            }   
        }
    }

puis dans vos vues où vous devez accéder à la propriété, ajoutez:

@using MyProject.Extensions

et appelez-le en cas de besoin par:

@User.GetFullName()

Le seul problème avec cela est que j'ai dû supprimer mon utilisateur de test actuel, puis me réinscrire pour voir le "FullName" même si la base de données contenait la propriété FullName.

14
willjohnathan

Je pense que vous devez utiliser la propriété Claims de l'utilisateur à cette fin. J'ai trouvé un bon article sur: http://benfoster.io/blog/customising-claims-transformation-in-aspnet-core-identity

Classe d'utilisateurs

public class ApplicationUser : IdentityUser
{
    public string MyProperty { get; set; }
}

Mettons MyProperty dans les revendications de l'utilisateur authentifié. À cette fin, nous remplaçons UserClaimsPrincipalFactory

public class MyUserClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
{
    public MyUserClaimsPrincipalFactory (
        UserManager<ApplicationUser> userManager,
        RoleManager<IdentityRole> roleManager,
        IOptions<IdentityOptions> optionsAccessor) : base(userManager, roleManager, optionsAccessor)
    {
    }

    public async override Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
    {
        var principal = await base.CreateAsync(user);

        //Putting our Property to Claims
        //I'm using ClaimType.Email, but you may use any other or your own
        ((ClaimsIdentity)principal.Identity).AddClaims(new[] {
        new Claim(ClaimTypes.Email, user.MyProperty)
    });

        return principal;
    }
}

Enregistrement de notre UserClaimsPrincipalFactory dans Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    //...
    services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, MyUserClaimsPrincipalFactory>();
    //...
}

Maintenant, nous pouvons accéder à notre propriété comme ceci

User.Claims.FirstOrDefault(v => v.Type == ClaimTypes.Email).Value;

Nous pouvons créer une extension

namespace MyProject.MyExtensions
{
    public static class MyUserPrincipalExtension
    {
        public static string MyProperty(this ClaimsPrincipal user)
        {
            if (user.Identity.IsAuthenticated)
            {
                return user.Claims.FirstOrDefault(v => v.Type == ClaimTypes.Email).Value;
            }

            return "";
        }
    }
}

Nous devons ajouter @Using à View (je l'ajoute à _ViewImport.cshtml global)

@using MyProject.MyExtensions

Et enfin, nous pouvons utiliser cette propriété dans n'importe quelle vue comme méthode d'appel

@User.MyProperty()

Dans ce cas, vous n'avez pas de requêtes supplémentaires à la base de données pour obtenir des informations sur l'utilisateur.

14
Alfer

OK, voici comment je l'ai finalement fait. J'ai utilisé une nouvelle fonctionnalité dans MVC6 appelée Afficher les composants . Celles-ci fonctionnent un peu comme des vues partielles, mais elles ont un " mini contrôleur " qui leur est associé. Le composant View est un contrôleur léger qui ne participe pas à la liaison de modèle, mais il peut lui être transmis quelque chose dans les paramètres du constructeur, éventuellement en utilisant l'injection de dépendances, puis il peut construire un modèle de vue et le transmettre à une vue partielle. Ainsi, par exemple, vous pouvez injecter une instance UserManager dans le composant View, l'utiliser pour récupérer l'objet ApplicationUser pour l'utilisateur actuel et le transmettre à la vue partielle.

Voici à quoi cela ressemble dans le code. Tout d'abord, le composant View, qui réside dans /ViewComponents répertoire:

public class UserProfileViewComponent : ViewComponent
    {
    readonly UserManager<ApplicationUser> userManager;

    public UserProfileViewComponent(UserManager<ApplicationUser> userManager)
        {
        Contract.Requires(userManager != null);
        this.userManager = userManager;
        }

    public IViewComponentResult Invoke([CanBeNull] ClaimsPrincipal user)
        {
        return InvokeAsync(user).WaitForResult();
        }

    public async Task<IViewComponentResult> InvokeAsync([CanBeNull] ClaimsPrincipal user)
        {
        if (user == null || !user.IsSignedIn())
            return View(anonymousUser);
        var userId = user.GetUserId();
        if (string.IsNullOrWhiteSpace(userId))
            return View(anonymousUser);
        try
            {
            var appUser = await userManager.FindByIdAsync(userId);
            return View(appUser ?? anonymousUser);
            }
        catch (Exception) {
        return View(anonymousUser);
        }
        }

    static readonly ApplicationUser anonymousUser = new ApplicationUser
        {
        Email = string.Empty,
        Id = "anonymous",
        PhoneNumber = "n/a"
        };
    }

Notez que le paramètre constructeur userManager est injecté par le framework MVC; ceci est configuré par défaut dans Startup.cs dans un nouveau projet donc il n'y a pas de configuration à faire.

Le composant view est invoqué, sans surprise, en appelant la méthode Invoke ou sa version asynchrone. La méthode récupère un ApplicationUser si possible, sinon elle utilise un utilisateur anonyme avec des paramètres par défaut sécurisés préconfigurés. Il utilise cet utilisateur pour sa vue partielle du modèle de vue. La vue vit dans /Views/Shared/Components/UserProfile/Default.cshtml et commence comme ceci:

@model ApplicationUser

<div class="dropdown profile-element">
    <span>
        @Html.GravatarImage(Model.Email, size:80)
    </span>
    <a data-toggle="dropdown" class="dropdown-toggle" href="#">
        <span class="clear">
            <span class="block m-t-xs">
                <strong class="font-bold">@Model.UserName</strong>
            </span> <span class="text-muted text-xs block">@Model.PhoneNumber <b class="caret"></b></span>
        </span>
    </a>

</div>

Et enfin, j'invoque cela depuis mon _Navigation.cshtml vue partielle comme ceci:

@await Component.InvokeAsync("UserProfile", User)

Cela répond à toutes mes exigences d'origine, car:

  1. J'effectue la recherche de base de données dans un contrôleur (un composant de vue est un type de contrôleur) et non dans une vue. De plus, les données peuvent bien être déjà en mémoire car le framework aura déjà authentifié la requête. Je n'ai pas cherché à savoir si un autre aller-retour dans la base de données se produisait réellement, et je ne m'en soucierai probablement pas, mais si quelqu'un le sait, veuillez sonner!
  2. La logique est dans un endroit bien défini; le principe DRY est respecté.
  3. Je n'ai à modifier aucun autre modèle de vue.

Résultat! J'espère que quelqu'un trouvera cela utile ...

2
Tim Long

J'ai le même problème et les mêmes préoccupations, mais j'ai choisi une solution différente à la place en créant une méthode d'extension pour ClaimsPrincipal et en laissant la méthode d'extension récupérer la propriété utilisateur personnalisée.

Voici ma méthode d'extension:

public static class PrincipalExtensions
{
    public static string ProfilePictureUrl(this ClaimsPrincipal user, UserManager<ApplicationUser> userManager)
    {
        if (user.Identity.IsAuthenticated)
        {
            var appUser = userManager.FindByIdAsync(user.GetUserId()).Result;

            return appUser.ProfilePictureUrl;
        }

        return "";
    }
}

Ensuite, dans ma vue (également la vue LoginPartial), j'injecte le UserManager puis transfère ce UserManager vers la méthode d'extension:

@inject Microsoft.AspNet.Identity.UserManager<ApplicationUser> userManager;
<img src="@User.ProfilePictureUrl(userManager)">

Cette solution, je crois, répond également à vos 3 exigences de séparation des préoccupations, DRY et aucun changement à aucun ViewModel. Cependant, même si cette solution est simple et peut être utilisée dans les vues standard non seulement ViewComponents, je suis Je ne suis toujours pas content. Maintenant, à mon avis, je peux écrire: @ User.ProfilePictureUrl (userManager), mais je pense que ce ne serait pas trop demander que je ne puisse écrire que: @ User.ProfilePictureUrl ().

Si seulement je pouvais rendre le UserManager (ou IServiceProvider) disponible dans ma méthode d'extension sans l'injecter de fonction, cela résoudrait le problème, mais je ne connais aucun moyen de le faire.

2
Rasmus Rummel

Comme on me l'a demandé, je poste ma solution éventuelle, bien que dans un projet différent (MVC5/EF6).

Tout d'abord, j'ai défini une interface:

public interface ICurrentUser
    {
    /// <summary>
    ///     Gets the display name of the user.
    /// </summary>
    /// <value>The display name.</value>
    string DisplayName { get; }

    /// <summary>
    ///     Gets the login name of the user. This is typically what the user would enter in the login screen, but may be
    ///     something different.
    /// </summary>
    /// <value>The name of the login.</value>
    string LoginName { get; }

    /// <summary>
    ///     Gets the unique identifier of the user. Typically this is used as the Row ID in whatever store is used to persist
    ///     the user's details.
    /// </summary>
    /// <value>The unique identifier.</value>
    string UniqueId { get; }

    /// <summary>
    ///     Gets a value indicating whether the user has been authenticated.
    /// </summary>
    /// <value><c>true</c> if this instance is authenticated; otherwise, <c>false</c>.</value>
    bool IsAuthenticated { get; }

Ensuite, je l'implémente dans une classe concrète:

/// <summary>
///     Encapsulates the concept of a 'current user' based on ASP.Net Identity.
/// </summary>
/// <seealso cref="MS.Gamification.DataAccess.ICurrentUser" />
public class AspNetIdentityCurrentUser : ICurrentUser
    {
    private readonly IIdentity identity;
    private readonly UserManager<ApplicationUser, string> manager;
    private ApplicationUser user;

    /// <summary>
    ///     Initializes a new instance of the <see cref="AspNetIdentityCurrentUser" /> class.
    /// </summary>
    /// <param name="manager">The ASP.Net Identity User Manager.</param>
    /// <param name="identity">The identity as reported by the HTTP Context.</param>
    public AspNetIdentityCurrentUser(ApplicationUserManager manager, IIdentity identity)
        {
        this.manager = manager;
        this.identity = identity;
        }

    /// <summary>
    ///     Gets the display name of the user. This implementation returns the login name.
    /// </summary>
    /// <value>The display name.</value>
    public string DisplayName => identity.Name;

    /// <summary>
    ///     Gets the login name of the user.
    ///     something different.
    /// </summary>
    /// <value>The name of the login.</value>
    public string LoginName => identity.Name;

    /// <summary>
    ///     Gets the unique identifier of the user, which can be used to look the user up in a database.
    ///     the user's details.
    /// </summary>
    /// <value>The unique identifier.</value>
    public string UniqueId
        {
        get
            {
            if (user == null)
                user = GetApplicationUser();
            return user.Id;
            }
        }

    /// <summary>
    ///     Gets a value indicating whether the user has been authenticated.
    /// </summary>
    /// <value><c>true</c> if the user is authenticated; otherwise, <c>false</c>.</value>
    public bool IsAuthenticated => identity.IsAuthenticated;

    private ApplicationUser GetApplicationUser()
        {
        return manager.FindByName(LoginName);
        }
    }

Enfin, je fais la configuration suivante dans mon noyau DI (j'utilise Ninject):

        kernel.Bind<ApplicationUserManager>().ToSelf()
            .InRequestScope();
        kernel.Bind<ApplicationSignInManager>().ToSelf().InRequestScope();
        kernel.Bind<IAuthenticationManager>()
            .ToMethod(m => HttpContext.Current.GetOwinContext().Authentication)
            .InRequestScope();
        kernel.Bind<IIdentity>().ToMethod(p => HttpContext.Current.User.Identity).InRequestScope();
        kernel.Bind<ICurrentUser>().To<AspNetIdentityCurrentUser>();

Ensuite, chaque fois que je veux accéder à l'utilisateur actuel, je l'injecte simplement dans mon contrôleur en ajoutant un paramètre constructeur de type ICurrentUser.

J'aime cette solution car elle résume bien la préoccupation et évite que mes contrôleurs aient une dépendance directe sur EF.

1
Tim Long

Vous devez effectuer une recherche (en utilisant par exemple Entity Framework) avec le nom de l'utilisateur actuel:

HttpContext.Current.User.Identity.Name
0
Rob Bos