我该如何从ASP.NET Core的视图中访问我的ApplicationUser属性?

32
我正在处理一个ASP.Net vNext/MVC6项目。我正在适应ASP.Net Identity。 ApplicationUser类是添加任何其他用户属性的地方,这与Entity Framework配合使用,并且我的额外属性按预期存储在数据库中。
但是,当我想从视图中访问当前登录用户的详细信息时,问题就来了。具体而言,我有一个名为_loginPartial.cshtml的文件,其中我想检索并显示用户的Gravatar图标,我需要电子邮件地址。
Razor View基类具有一个User属性,它是一个ClaimsPrincipal。我怎么能从这个User属性返回到我的ApplicationUser,以检索自定义属性?
请注意,我不是在询问如何查找信息;我知道如何从User.GetUserId()值查找ApplicationUser。这更多是关于如何明智地解决这个问题的问题。具体而言,我不想:
- 在我的视图中执行任何数据库查找(关注点分离) - 必须在每个控制器中添加逻辑以检索当前用户的详细信息(DRY原则) - 必须向每个ViewModel添加一个User属性。
这似乎是一个“跨领域”的问题,应该有一个集中的标准解决方案,但我感觉我缺少了拼图的一部分。从视图中访问这些自定义用户属性的最佳方法是什么?
注意:在项目模板中,MVC团队似乎已经绕过了这个问题,通过确保UserName属性始终设置为用户的电子邮件地址,巧妙地避免了需要查找以获取用户的电子邮件地址的需要!对我来说,这似乎有点欺骗性,在我的解决方案中,用户的登录名可能是他们的电子邮件地址,也可能不是,因此我不能依赖这个技巧(我怀疑以后还会有其他我需要访问的属性)。

答案很复杂,我现在没有时间回答,但是有一些想法。
  1. 我相信你添加的任何内容都可以在对象的某个地方访问到。我过去做过这样的事情,只是不知道在哪里,需要找到旧代码。
关于你的问题:
  1. 没有更多的代码,无法判断,但我认为这不是默认情况。
  2. 你可以在一个位置添加代码,在每个请求中检索用户;我需要找到一个教程。
  3. 你不必这样做,但你可能想使用视图模型来代替身份对象。
- drneel
1
我觉得我可能有所发现:视图组件。在MVC6中,它们允许部分视图拥有自己的控制器,并且可以注入UserManager。还需要进行更多的实验,除非有人先提供了更好的答案,否则我会回复并分享我找到的内容。 - Tim Long
请在您想到可行的解决方案时更新或添加您自己的答案。谢谢。 - Chad
我会暂停进一步的开发工作,直到微软发布 ASP.Net Core RC2 版本。 - Tim Long
嗨,Tim,你是怎么做到的?“我知道如何通过User.GetUserId()值查找ApplicationUser”。在asp.net core final中似乎没有扩展方法。 - Pascal
显示剩余5条评论
6个回答

25
最初回答的更新:(这违反了原帖作者的第一个要求,请参考我的原始答案,如果您有相同的要求)在Razor视图中引用FullName,而无需修改声明并添加扩展文件(在我原始的解决方案中),即可完成此操作。
@UserManager.GetUserAsync(User).Result.FullName

这基本上只是一个较短的示例,与此stackoverflow问题和该教程相似。假设您已经在“ApplicationUser.cs”中设置了属性,并且为注册提供了适用的ViewModels和Views。
例如,使用“FullName”作为额外属性:
将“AccountController.cs”的Register方法修改为:
    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;
            }   
        }
    }

"最初的回答":在您需要访问该属性的视图中添加以下代码:

然后在你的视图中,需要访问该属性时添加:

@using MyProject.Extensions

最初的回答:并在需要时调用它:
@User.GetFullName()

这样做的一个问题是,我不得不删除我的当前测试用户,然后重新注册,才能看到"FullName",尽管数据库中已经有FullName属性。"Original Answer"的翻译是"最初的回答"。

我以前用过这个方法,但它会导致我的授权标签失效,请不要使用这种方法。 - c-sharp-and-swiftui-devni

18

我认为你应该使用用户的Claims属性来实现此目的。我找到了一篇好的文章:http://benfoster.io/blog/customising-claims-transformation-in-aspnet-core-identity

User类

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

让我们将MyProperty放入经过身份验证的用户的Claims中。为此,我们要重写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(我将其添加到全局 _ViewImport.cshtml 中)。
@using MyProject.MyExtensions

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

@User.MyProperty()
在这种情况下,您无需向数据库发出额外的查询以获取用户信息。

这有一个很大的问题!如果您与用户表具有一对一的关系,并且需要从第二个表中获取某些属性,则无法实现!例如,使用此方法,user.operator.Name始终为空! - Hatef.

2

好的,这是我最终的解决方案。我使用了MVC6中的一个新功能,叫做View Components。它们的工作原理有点像部分视图,但是它们有一个与之相关的"微型控制器"。View Component是一个轻量级的控制器,它不参与模型绑定,但是可以在构造函数参数中传递一些东西并可能使用依赖注入来构建视图模型并将其传递给部分视图。例如,您可以将一个UserManager实例注入到View Component中,使用它来检索当前用户的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(如果可能),否则使用一些安全默认值预先配置的匿名用户。它将使用此用户作为局部视图的视图模型。视图位于/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是控制器的一种),而不是在视图中。此外,数据可能已经存在于内存中,因为框架已经验证了请求。我没有研究是否实际发生了另一个数据库回传,但如果有人知道,请介入!
  2. 逻辑在一个明确定义的地方;DRY原则得到尊重。
  3. 我不必修改任何其他视图模型。

结果!我希望有人能从中受益...


我现在已经放弃了这个解决方案。相反,我定义了一个接口ICurrentUser和一个具体类AspNetIdentityCurrentUser。所有的查询逻辑都包含在具体类中。然后,在应用程序启动时,我只需将我的ICurrentUser接口注册为服务,然后无论何时需要,我都可以轻松地注入它。当然,魔鬼在细节中... - Tim Long

2
我有同样的问题和担忧,不过我选择了一个不同的解决方案,创建了一个扩展方法到ClaimsPrincipal,让这个扩展方法检索自定义用户属性。
以下是我的扩展方法:
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 "";
    }
}

在我的看法中(也是LoginPartial视图),我注入了UserManager,然后将该UserManager传递给扩展方法:

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

我相信这个解决方案也符合你的三个要求:关注点分离、DRY原则和不对任何ViewModel进行更改。然而,虽然这个解决方案简单,可以在标准视图中使用,但我仍然不满意。现在在我的视图中,我可以写: @User.ProfilePictureUrl(userManager),但我认为只写@User.ProfilePictureUrl()也不过分。
如果我能够在不进行函数注入的情况下使UserManager(或IServiceProvider)在我的扩展方法中可用,那就可以解决问题了,但我不知道如何做到这一点。

有趣的方法。我正在尝试理解什么是 .GetUserId()。 - c-sharp-and-swiftui-devni

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。


0

你需要使用实体框架等工具,通过当前用户的名称进行搜索:

HttpContext.Current.User.Identity.Name

假设我将在每个视图中显示相同的信息(通过 _layout.cshtml 中的部分视图),显然每次都明确地进行数据库查找是很疯狂的。请注意,我不是在问如何查找信息,我知道如何从 User.GetUserId() 值查找 ApplicationUser。这更多是一个关于如何明智地处理而不是在视图中拥有数据访问代码的问题。是否有办法为每个视图标准化这个过程? - Tim Long
我们通常将这些信息保存到一个会话对象中,并重复使用该对象。 - Rob Bos
明白了 - 谢谢。我会把这个技巧记在心里,以备不时之需。虽然我更喜欢不使用会话状态。当我学习MVC时,早期就被灌输了这个思想,到目前为止我一直坚持这个原则...顺便说一下,我已经用我最终采用的解决方案回答了自己的问题。 - Tim Long

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