【问题标题】:How should I access my ApplicationUser properties from ASP.NET Core Views?我应该如何从 ASP.NET Core 视图访问我的 ApplicationUser 属性?
【发布时间】:2016-03-03 19:36:18
【问题描述】:

我正在开发一个 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. 您不必这样做,但您可能想要使用视图模型而不是 Identity 对象。
  • 我想我可能正在做一些事情:查看组件。在 MVC6 中,这些允许部分视图拥有自己的控制器,可以将 UserManager 注入其中。需要更多的实验,除非有人先提出更好的答案,否则我会发回我的发现。
  • 我可以通过多种方式看到这种情况发生。您可以使用所需的值向主体添加声明,也可以在应用程序用户上创建扩展方法以获取视图中的这些值。
  • 当您提出可行的解决方案时,请更新或添加您自己的答案。谢谢。
  • 在微软发布 ASP.Net 核心的 RC2 之前,我将推迟进一步的开发。

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


【解决方案1】:

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

@UserManager.GetUserAsync(User).Result.FullName

原答案:

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

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

使用“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;
            }   
        }
    }

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

@using MyProject.Extensions

并在需要时调用它:

@User.GetFullName()

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

【讨论】:

  • 我之前有过这个,它阻止了我的授权标签工作不要使用这种方法。
【解决方案2】:

我认为您应该为此目的使用 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

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

@User.MyProperty()

在这种情况下,您无需对数据库进行额外查询以获取用户信息。

【讨论】:

  • 这有大问题!如果您与用户表有一对一的关系,并且如果您需要从第二个表中获取一些属性,那么您不能!例如 user.operator.Name 用这种方式总是为空!
【解决方案3】:

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

这是它在代码中的样子。首先是视图组件,它位于/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. 我在控制器(视图组件是一种控制器)中而不是在视图中执行数据库查找。此外,数据很可能已经在内存中,因为框架已经验证了请求。我还没有调查是否真的发生了另一个数据库往返,我可能不会打扰,但如果有人知道,请插话!
  2. 逻辑位于一个明确定义的位置;尊重 DRY 原则。
  3. 我不需要修改任何其他视图模型。

结果!我希望有人会觉得这很有用...

【讨论】:

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

我有同样的问题和同样的顾虑,但是我选择了不同的解决方案,而不是为 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)">

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

如果我可以在没有函数注入的情况下使 UserManager(或 IServiceProvider)在我的扩展方法中可用,它会解决问题,但我不知道该怎么做。

【讨论】:

  • 有趣的方法我想了解什么是 .GetUserId()
【解决方案5】:

正如我被问到的那样,我将发布我的最终解决方案,尽管是在不同的 (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。

【讨论】:

    【解决方案6】:

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

    HttpContext.Current.User.Identity.Name
    

    【讨论】:

    • 假设我将在每个视图中显示相同的信息(通过 _layout.cshtml 中的部分视图),每次都明确地进行数据库查找似乎很疯狂。请注意,我不是在问如何查找信息,我知道如何从 User.GetUserId() 值中查找 ApplicationUser。这更多的是关于如何明智地处理这个问题,而不是在视图中拥有数据访问代码。有没有办法为每个视图标准化?
    • 我们通常将这些信息保存到一个会话对象中并重复使用该对象。
    • 有道理 - 谢谢。我会把这个把戏放在我的后兜里,以备不时之需。我不喜欢使用会话状态。当我学习 MVC 时,我很早就意识到了这一点,这是我迄今为止一直坚持的事情......仅供参考,我用我最终使用的解决方案回答了我自己的问题。
    猜你喜欢
    • 2018-04-13
    • 2021-05-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-09-10
    • 1970-01-01
    • 2013-01-23
    • 1970-01-01
    相关资源
    最近更新 更多