我应该如何从MVC 6视图中访问我的ApplicationUser属性?

时间:2016-03-03 19:36:18

标签: c# asp.net-mvc asp.net-identity asp.net-core-mvc

我正在开发一个ASP.Net vNext / MVC6项目。我正在掌握ASP.Net Identity。

ApplicationUser类显然是我应该添加任何其他用户属性的地方,这适用于Entity Framework,我的其他属性按预期存储在数据库中。

但是,当我想从我的视图中访问当前登录用户的详细信息时,问题就出现了。具体来说,我有_loginPartial.cshtml我要检索并显示用户的Gravatar图标,我需要该电子邮件地址。

Razor View基类具有User属性,即ClaimsPrincipal。如何从此User属性返回到我的ApplicationUser,以检索我的自定义属性?

请注意,我不会询问如何查找信息;我知道如何从ApplicationUser值中查找User.GetUserId()。这是一个关于如何合理地解决这个问题的问题。具体来说,我不想:

  • 从我的视图中进行任何类型的数据库查找(关注点分离)
  • 必须为每个控制器添加逻辑以检索当前用户的详细信息(DRY原则)
  • 必须为每个ViewModel添加一个User属性。

这似乎是一个跨领域的问题'应该有一个集中的标准解决方案,但我觉得我错过了一块拼图游戏。从视图中获取这些自定义用户属性的最佳方法是什么?

注意:似乎MVC团队通过确保UserName属性始终设置为用户的电子邮件地址,在项目模板中侧面解决了这个问题,巧妙地避免了他们执行此查找的必要性获取用户的电子邮件地址!这对我来说似乎有点欺骗,在我的解决方案中,用户的登录名可能是也可能不是他们的电子邮件地址,所以我不能依赖这个技巧(我怀疑会有其他的)我需要稍后访问的属性。

6 个答案:

答案 0 :(得分:14)

我认为您应该为此目的使用User的Claims属性。我找到了关于:http://benfoster.io/blog/customising-claims-transformation-in-aspnet-core-identity

的好帖子

用户类

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

让我们将MyProperty置于认证用户的声明中。为此,我们重写了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;
    }
}

在Startup.cs中注册我们的UserClaimsPrincipalFactory

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

现在我们可以像这样访问我们的这个

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

我们可能会创建一个扩展名

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

我们应该将@Using添加到View(我将其添加到全局_ViewImport.cshtml)

@using MyProject.MyExtensions

最后,我们可以在任何View中使用此属性作为方法调用

@User.MyProperty()

在这种情况下,您没有额外的数据库查询来获取用户信息。

答案 1 :(得分:14)

更新为原始回答:(这违反了操作的第一个要求,如果您有相同的要求,请参阅我的原始答案)您可以在不修改声明和添加扩展文件的情况下执行此操作(在我的原始解决方案)通过在Razor视图中引用FullName为:

@UserManager.GetUserAsync(User).Result.FullName

原始答案:

这只是this stackoverflow question的一个较短示例,并且遵循此tutorial

假设您已经在“ApplicationUser.cs”中设置了属性以及适用的ViewModel和Views进行注册。

使用“FullName”作为额外属性的示例:

将“AccountController.cs”注册方法修改为:

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

然后我添加了一个新文件“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;
            }   
        }
    }

然后在您的视图中,您需要访问属性add:

@using MyProject.Extensions

并在需要时通过以下方式调用:

@User.GetFullName()

这个问题的一个问题是我必须删除当前的测试用户,然后重新注册,以便查看“FullName”,即使数据库中有FullName属性。

答案 2 :(得分:2)

好的,这是我最终如何做到的。我在MVC6中使用了一个名为 View Components 的新功能。这些工作有点像部分视图,但他们有一个&#34; 迷你控制器&#34;与他们相关联。 View Component是一个轻量级控制器,它不参与模型绑定,但它可以在构造函数参数中传递一些东西,可能使用依赖注入,然后它可以构造一个View Model并将其传递给一个局部视图。因此,例如,您可以将UserManager实例注入View组件,使用它来检索当前用户的ApplicationUser对象并将其传递给局部视图。

这是代码中的样子。首先是View Component,它位于/ViewComponents目录:

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

请注意,userManager构造函数参数由MVC框架注入;默认情况下,这是在新项目的Startup.cs中配置的,因此无法完成任何配置。

通过调用Invoke方法或其异步版本,可以毫不奇怪地调用视图组件。如果可能,该方法将检索ApplicationUser,否则它将使用具有一些安全defaultspreconfigured的匿名用户。它将此用户用于其视图模型的partiel视图。该视图位于/Views/Shared/Components/UserProfile/Default.cshtml,并以如下所示开始:

@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>

最后,我在我的_Navigation.cshtml局部视图中调用了这个,如下所示:

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

这符合我原来的所有要求,因为:

  1. 我在控制器中执行数据库查找(View Component是一种控制器),而不是在View中。此外,数据可能已经在内存中,因为框架已经对请求进行了身份验证。我还没有看到另一个数据库往返是否真的发生了,我可能不会打扰但是如果有人知道的话,请加入!
  2. 逻辑在一个明确定义的地方; DRY原则受到尊重。
  3. 我不必修改任何其他视图模型。
  4. 结果!我希望有人会觉得这很有用......

答案 3 :(得分:2)

我有同样的问题和相同的问题,但是我选择了另一种解决方案,而不是创建一个ClaimPrincipal的扩展方法,并让扩展方法检索自定义用户属性。

这是我的扩展方法:

dll.a

接下来在我的视图(也是LoginPartial视图)中,我注入UserManager,然后将UserManager传输到扩展方法:

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

我相信这个解决方案也符合您对关注点分离的3个要求,DRY并且不会对任何ViewModel进行任何更改。然而,虽然这个解决方案很简单,并且可以在标准视图中使用,而不仅仅是ViewComponents,我仍然不满意。现在在我看来我可以写: @ User.ProfilePictureUrl(userManager),但我认为我应该只能写下来并不会太多: 的 @ User.ProfilePictureUrl()

如果只有我可以在我的扩展方法中使用UserManager(或IServiceProvider)而没有函数注入它,它将解决问题,但我知道没办法这样做。

答案 4 :(得分:1)

正如我被问到的那样,我发布了我的最终解决方案,尽管是在另一个(MVC5 / EF6)项目中。

首先,我定义了一个界面:

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

然后,我在具体的类中实现它:

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

最后,我在DI内核中进行了以下配置(我正在使用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>();

然后每当我想访问当前用户时,我只需通过添加ICurrentUser类型的构造函数参数将其注入我的控制器。

我喜欢这个解决方案,因为它很好地封装了关注点并避免了我的控制器直接依赖于EF。

答案 5 :(得分:0)

您需要使用当前用户的名称进行搜索(例如使用实体框架):

HttpContext.Current.User.Identity.Name