web-dev-qa-db-ja.com

ASP.NET CoreビューからApplicationUserプロパティにアクセスするにはどうすればよいですか?

ASP.Net vNext/MVC6プロジェクトに取り組んでいます。私はASP.Net Identityを理解しています。

ApplicationUserクラスは、追加のユーザープロパティを追加することになっているようです。これはEntity Frameworkで機能し、追加のプロパティは期待どおりにデータベースに格納されます。

ただし、現在ログインしているユーザーの詳細にビューからアクセスしたい場合に問題が発生します。具体的には、ユーザーのGravatarアイコンを取得して表示する__loginPartial.cshtml_があります。このアイコンにはメールアドレスが必要です。

Razor View基本クラスには、ClaimsPrincipalであるUserプロパティがあります。このUserプロパティからApplicationUserに戻り、カスタムプロパティを取得するにはどうすればよいですか?

情報を見つける方法を尋ねているのではないことに注意してください。 User.GetUserId()値からApplicationUserをルックアップする方法を知っています。これは、この問題に賢明に対処する方法についての質問です。具体的には、私はしたくない:

  • 私のビュー内からあらゆる種類のデータベース検索を実行します(懸念の分離)
  • 現在のユーザーの詳細を取得するために、すべてのコントローラーにロジックを追加する必要があります(DRY原則)
  • すべてのViewModelにUserプロパティを追加する必要があります。

これは一元化された標準ソリューションを持つべき「横断的関心事」のように思えますが、ジグソーパズルの一部を見逃しているように感じます。ビュー内からカスタムユーザープロパティを取得する最良の方法は何ですか?

注:MVCチームは、UserNameプロパティが常にユーザーの電子メールアドレスに設定されるようにすることで、プロジェクトテンプレート内でこの問題を回避し、ユーザーがこのルックアップを実行してユーザーの電子メールアドレスを取得する必要性をきちんと回避しているようです!それは私には少しカンニングのようで、私のソリューションでは、ユーザーのログイン名は電子メールアドレスである場合とそうでない場合があるため、そのトリックに頼ることはできません(そして、後でアクセスする必要がある他のプロパティがあると思います)。

19
Tim Long

元の回答への更新:(これはopの最初の要件に違反しています。同じ要件がある場合は元の回答を参照してください)クレームを変更せずに実行できますRazorビューでFullNameを次のように参照して、拡張ファイルを追加します(元のソリューションで)。

@UserManager.GetUserAsync(User).Result.FullName

元の答え:

これは this stackoverflow question とこれに続く tutorial のほんの短い例です。

「ApplicationUser.cs」で設定されたプロパティと、登録用の該当するViewModelおよびビューがすでにあると仮定します。

追加のプロパティとして「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()

これに関する1つの問題は、データベースにFullNameプロパティが含まれていても、現在のテストユーザーを削除し、「FullName」を表示するために再登録する必要があることです。

14
willjohnathan

そのためには、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を追加する必要があります(グローバル_ViewImport.cshtmlに追加します)

@using MyProject.MyExtensions

そして最後に、任意のビューでこのプロパティをメソッド呼び出しとして使用できます

@User.MyProperty()

この場合、ユーザー情報を取得するためのデータベースへの追加クエリはありません。

14
Alfer

OK、最終的には次のようになりました。 View Componentsと呼ばれるMVC6の新機能を使用しました。これらは部分ビューのように機能しますが、「ミニコントローラー」が関連付けられています。 Viewコンポーネントは、モデルバインディングに参加しない軽量のコントローラーですが、おそらく依存関係の挿入を使用して、コンストラクターパラメーターで何かを渡すことができ、View Modelを構築してそれを部分ビューに渡すことができます。そのため、たとえば、ViewコンポーネントにUserManagerインスタンスを挿入し、それを使用して現在のユーザーのApplicationUserオブジェクトを取得し、それを部分ビューに渡すことができます。

コードでは次のようになります。まず、/ViewComponentsディレクトリにあるViewコンポーネント:

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コンポーネントはコントローラーの一種です)でデータベース検索を実行しています。さらに、フレームワークはすでにリクエストを認証しているため、データはすでにメモリ内にある可能性があります。別のデータベースラウンドトリップが実際に発生するかどうかは検討していません。おそらく気にすることはないでしょうが、誰かが知っている場合はチャイムしてください。
  2. ロジックは明確に定義された1つの場所にあります。 DRY原則が尊重されます。
  3. 他のビューモデルを変更する必要はありません。

結果!誰かがこれが役立つと思います...

2
Tim Long

私は同じ問題と同じ懸念を持っていますが、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)を使用可能にできれば、問題は解決しますが、それを行う方法はわかりません。

2
Rasmus Rummel

それについて尋ねられたので、別の(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に直接依存することを避けるため、気に入っています。

1
Tim Long

現在のユーザーの名前で検索を実行する必要があります(Entity Frameworkなどを使用):

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