WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

ASP.NET Core MVC の非同期プログラミング

by WebSurfer 2. January 2021 17:40

先の記事「ASP.NET MVC の非同期プログラミング」は .NET Framework 版の MVC5 アプリの話ですが、ASP.NET Core 3.1 MVC アプリでも同様なことを検証しましたのでその結果を書きます。

ASP.NET Core 3.1 MVC の非同期プログラミング

以下に (1) async / await を利用した非同期プログラミングで使用されるスレッドがどのようになるか、(2) Task.Result などを使った同期コードと非同期コードを混在させるとデッドロックは起きるのか、(3) Task.ConfigureAwait(Boolean) メソッドを付与するとどう変わるかついて書きます。

意外だったのは、.NET Framework 版と違って、Task.Result を使ってもデッドロックならないということででした。

なお、Kestrel の場合はどうなるかですが、Visual Studio を使って IIS Express (インプロセス ホスティング) と Kestrel を切り替えて両方の動作を確認しました。どちらも同じ結果となりました。

(1) 使用されるスレッド

ASP.NET で非同期プログラミングを行う目的は、スレッドプールにある限られた数のスレッドを有効利用し、スループットを向上するためです。なので非同期メソッドのチェーンの一番深いところにある await 前後でスレッドが切り替わるはずです。

それを確認するために以下のコードで試してみた結果が上の画像です。期待通り、TimeCosumingMethod メソッドの await 前後で ManagedThreadId が 27 から 25 に変わっています。

public async Task<ActionResult> AsyncTest()
{
    ViewBag.Id1 = "開始時, ID=" +
                  Thread.CurrentThread.ManagedThreadId;

    ViewBag.Id2 = await TimeCosumingMethod();

    // .NET Framework 版の MVC5 アプリでは上に代えて以下のようにすると
    // デッドロックになったが Core 3.1 版ではデッドロックにはならない
    //ViewBag.Id2 = TimeCosumingMethod().Result;

    ViewBag.Id3 = "終了時, ID=" +
                  Thread.CurrentThread.ManagedThreadId;

    return View();
}

private async Task<string> TimeCosumingMethod()
{
    int id = Thread.CurrentThread.ManagedThreadId;

    await Task.Delay(3000);

    // ConfigureAwait(false) の付与は結果に関係なし。
    // デッドロックになる時は ConfigureAwait(false) が勝手に付与され、
    // デッドロックにならない時は付与しても無視されるような感じ
    //await Task.Delay(3000).
    //    ConfigureAwait(continueOnCapturedContext: false);

    return "TimeCosumingMethod の戻り値, ID(IN)=" + id +
           " / ID(OUT)=" + Thread.CurrentThread.ManagedThreadId;
}

(2) Task.Result の使用

先の記事「await と Task.Result によるデッドロック」で書いたように Task.Result を使った同期コードと非同期コードを混在させるとデッドロックは起きるのかということの確認です。

結果は下の画像の通り処理は無事完了し、デッドロックは起きなかったです。.NET Framework 版の MVC5 アプリで TimeCosumingMethod の await Task.Delay(3000); に .ConfigureAwait(false) を付与した場合と同じ結果です。すなわち ID(OUT) のみ ManagedThreadId が異なり他は同じになっています。

Task.Result の使用

Task.Result をどのように使ったかは上のコードの AsyncTest アクションメソッドのコメントを見てください。.NET Framework 版での検証と全く同じやり方ですです。

.NET Framework 版でデッドロックなる理由は: まず、TimeCosumingMethod().Result で 1 つの同期ブロックが待機中となる。呼び出された TimeCosumingMethod メソッドの await で待機する際にキャプチャされた「現在のコンテキスト」で await 完了後の同期処理を実行しようとする。しかし「現在のコンテキスト」では一度に実行するコードは 1 つのチャンクに限定されている。なので、Result プロパティでの待機が終わるまで await 完了後の同期処理は実行できない。結果デッドロックになる・・・ということで、Core 3.1 でも同じになると信じていたんですが、一体どうなっているのでしょう?

(3) ConfigureAwait の付与

AsyncTest アクションメソッドで Result を使うか否かで結果は上の画像のように変わりますが、上のコードの TimeCosumingMethod のコメントに書きましたように、ConfigureAwait(false) の付与はその結果に関係なかったです。

Core では AsyncTest アクションメソッドで Result を使ってデッドロックになる時は ConfigureAwait(false) が勝手に付与され、正しく await してデッドロックにならない時は ConfigureAwait(false) を付与しても無視されているような感じです。

.NET Framework 版とは話が大きく変ってきてしまうのですが、一体どうなっているのでしょう。どうも今までの知識は Core には役に立たないようで、また勉強しなければならないようです。でも、今はその気力がないです。(笑)

Tags: , , , , ,

CORE

ASP.NET MVC の非同期プログラミング

by WebSurfer 4. October 2020 15:50

ASP.NET MVC アプリで async / await を利用した非同期プログラミングで (1) 使用されるスレッドがどのようになるか、(2) Task.Result などを使った同期コードと非同期コードを混在させるとデッドロックは起きるのか、(3) ConfigureAwait メソッドでデッドロックは回避できるのかについて書きます。(.NET Framework アプリの話です。ASP.NET Core 3.1 MVC アプリの検証結果は別の記事に書きました)

ASP.NET MVC の非同期プログラミング

ちなみに ASP.NET Web Forms アプリ用の HTTP ハンドラで async / await を使って非同期呼び出しをする話は先の記事「非同期 HTTP ハンドラ (2)」に書きましたので興味があればそちらを見てください。

(1) 使用されるスレッド

ASP.NET で非同期プログラミングを行う目的はスレッドプールにある限られた数のスレッドを有効利用しスループットを向上するためです。そこが UI の応答性の向上を目的としている Windows Forms のような GUI アプリと違うところです。

ASP.NET アプリでは Web サーバーがクライアントから要求を受けるとスレッドプールからスレッドを確保して要求を処置します。非同期操作をしなければ、要求を受けてから応答を返すまで最初に確保したスレッドを保持し続けます。

Web アプリでは、外部のデータベースや Web API などにアクセスしてデータを取得するということが多いと思いますが、それに時間がかかる場合は一旦使っていたスレッドはスレッドプールに戻し、データ取得後の処理はスレッドプールから新たにスレッドを取得して行うようにすればスレッドプールのスレッドの有効利用が可能です。

そのあたりの詳細は Microsoft のドキュメント「ASP.NET の非同期/待機の概要」に図解入りで説明されているので見てください。

非同期プログラミングを行うと await 前後で実���にスレッドは違うのかを ASP.NET MVC アプリで試した結果が上の画像です。そのコードは以下の通りです。

public async Task<ActionResult> AsyncTest()
{
    ViewBag.Id1 = "開始時, ID=" + 
                  Thread.CurrentThread.ManagedThreadId;

    ViewBag.Id2 = await TimeCosumingMethod();

    ViewBag.Id3 = "終了時, ID=" + 
                  Thread.CurrentThread.ManagedThreadId;

    return View();
}

private async Task<string> TimeCosumingMethod()
{
    int id = Thread.CurrentThread.ManagedThreadId;

    await Task.Delay(3000);

    return "TimeCosumingMethod の戻り値, ID(IN)=" + id + 
           " / ID(OUT)=" + Thread.CurrentThread.ManagedThreadId;
}

上の画像の ID の数字 (ManagedThreadId) を見てください。TimeCosumingMethod メソッドの await 前後で ManagedThreadId が 6 から 7 に変わっています。ちなみに、Windows Forms のような GUI アプリでは await 前後いずれも UI スレッドになり ManagedThreadId は変わりません。

ASP.NET でも await で待機するときに現在のコンテキストがキャプチャされ、await 完了後はキャプチャしたコンテキストで続きの処理が行われるのは GUI アプリと同様だそうですが、await 前後で同じになるようにしているのはスレッドではなく HttpContext だそうです。それは仕組み上当たり前&そうせざるを得ないと思います。

(2) Task.Result でデッドロック

先の記事「await と Task.Result によるデッドロック」で書いたような Task.Result を使った同期コードと非同期コードを混在させるとデッドロックは起きるでしょうか?

その記事にも少し書きましたが、上のコードの await TimeCosumingMethod() を TimeCosumingMethod().Result に代えるとデッドロックは起きます。そのメカニズムは以下のようなことであろうと思います。

まず、TimeCosumingMethod().Result で 1 つの同期ブロックが待機中となる。呼び出された TimeCosumingMethod メソッドの await で待機する際にキャプチャされた「現在のコンテキスト」で await 完了後の同期処理を実行しようとする。しかし「現在のコンテキスト」では一度に実行するコードは 1 つのチャンクに限定されている。なので、Result プロパティでの待機が終わるまで await 完了後の同期処理は実行できない。結果デッドロックになる。

(3) ConfigureAwait でデッドロック回避

先の記事「ConfigureAwait によるデッドロックの回避」で書いたように、await 完了後の同期処理を実行するのに、await で待機する際にキャプチャした「現在のコンテキスト」ではなく、別のコンテキストで行えばデッドロックにはなりません。

ConfigureAwait メソッドの使用

以下のコードのように ConfigureAwait(false) を追加することにより、await 完了後の残り処理は、キャプチャしたコンテキストではなく、スレッドプールのコンテキストで処理されるのでデッドロックは回避でき、上の画像のとおり実行が完了します。

public async Task<ActionResult> AsyncTest()
{
    ViewBag.Id1 = "開始時, ID=" + 
                  Thread.CurrentThread.ManagedThreadId;

    ViewBag.Id2 = TimeCosumingMethod().Result;

    ViewBag.Id3 = "終了時, ID=" + 
                  Thread.CurrentThread.ManagedThreadId;

    return View();
}

private async Task<string> TimeCosumingMethod()
{
    int id = Thread.CurrentThread.ManagedThreadId;

    // ConfigureAwait(false) を追加するとデッドロックは回避できる
    await Task.Delay(3000).
        ConfigureAwait(continueOnCapturedContext: false);

    return "TimeCosumingMethod の戻り値, ID(IN)=" + id + 
           " / ID(OUT)=" + Thread.CurrentThread.ManagedThreadId;
}

ただし、スレッドは await 前後で同じになります。ということは、要求を受けた時に確保したスレッドを応答を返すまでずっと使い続けていたということで、スレッドの有効利用という ASP.NET の非同期の目的は果たせてないようです。

await 前後でスレッドが異なる場合は、await 前にキャプチャしたコンテキストを await 後でも使わないと HttpContext が渡せないが、continueOnCapturedContext: false ではそれができないので同じスレッドを使い続けざるを得ないということではないかと思います。

Tags: , , ,

MVC

ASP.NET Core Identity 独自実装(その2)

by WebSurfer 6. September 2020 16:29

先の記事「ASP.NET Core Identity 独自実装(その1)」と「Register, Login, Logout 機能の実装」の続きです。

ロール管理画面

先の記事では ASP.NET Core Identity のユーザー認証に用いるデータソースとカスタムプロバイダを作成し、それを用いてユーザー認証機能を実装しました。今回、ロール機能を追加しましたので、その方法などを備忘録として書いておきます。

先の記事「ASP.NET Core Identity 独自実装(その1)」の一番上に表示した画像を見てください。前回、その画像の Data Source, Data Access Layer レイヤーおよび Identity Store レイヤーの中のカスタムプロバイダ UserStore までは作成し、期待通りの動作をすることは確認しました。

今回、ロール機能を実現するためのカスタムプロバイダ RoleStore を実装し、先に作成したカスタムプロバイダ UserStore にロールによるアクセス制限のための機能を追加します。

(1) RoleStore クラスの実装

先に骨組みだけ作っておきましたが、それに中身を実装します。作成済みの UserStore のメソッドを参考にしました。

上位レイヤーにある RoleInManager がこのクラスに定義したメソッドを使って、先に作成済みの Data Access Layer を介して SQL Server の Role テーブルにアクセスし、ロールデータの作成、取得、削除などの操作を行います。

using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using MvcIdCustom.Models;

namespace MvcIdCustom.DAL
{
    public class RoleStore : IRoleStore<Role>, 
                             IQueryableRoleStore<Role>
    {
        private DataContext db;

        public RoleStore(DataContext db)
        {
            this.db = db;
        }

        public void Dispose()
        {
            Dispose(true);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (db != null)
                {
                    db.Dispose();
                    db = null;
                }
            }
        }

        // IQueryableRoleStore<Role> のメンバーの Roles プロパティ
        public IQueryable<Role> Roles
        {
            get { return db.Roles.Select(r => r); }
        }

        // 同じロール名の二重登録の防止機能は上位レイヤーに組み込まれている
        public async Task<IdentityResult> CreateAsync(Role role, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (role == null) throw new ArgumentNullException(nameof(role));

            db.Add(role);

            int rows = await db.SaveChangesAsync(cancellationToken);

            if (rows > 0)
            {
                return IdentityResult.Success;
            }

            return IdentityResult.Failed(
                new IdentityError { Description = $"{role.Name} 登録失敗" });
        }

        // 同じロール名の二重登録の防止機能は上位レイヤーに組み込まれている
        public async Task<IdentityResult> UpdateAsync(Role role, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (role == null) throw new ArgumentNullException(nameof(role));

            db.Update(role);

            int rows = await db.SaveChangesAsync(cancellationToken);

            if (rows > 0)
            {
                return IdentityResult.Success;
            }

            return IdentityResult.Failed(
                new IdentityError { Description = $"{role.Name} 更新失敗" });
        }

        public async Task<IdentityResult> DeleteAsync(Role role, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (role == null) throw new ArgumentNullException(nameof(role));

            db.Remove(role);

            int rows = await db.SaveChangesAsync(cancellationToken);

            if (rows > 0)
            {
                return IdentityResult.Success;
            }

            return IdentityResult.Failed(
                new IdentityError { Description = $"{role.Name} 削除失敗" });
        }

        public Task<string> GetRoleIdAsync(Role role, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (role == null) throw new ArgumentNullException(nameof(role));

            return Task.FromResult(role.Id.ToString());
        }

        public Task<string> GetRoleNameAsync(Role role,
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (role == null) throw new ArgumentNullException(nameof(role));

            return Task.FromResult(role.Name);
        }

        public Task SetRoleNameAsync(Role role, 
            string roleName, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (role == null) throw new ArgumentNullException(nameof(role));
            if (string.IsNullOrEmpty(roleName)) 
                throw new ArgumentException("roleName");

            role.Name = roleName;

            return Task.CompletedTask;
        }

        public Task<string> GetNormalizedRoleNameAsync(Role role, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (role == null) throw new ArgumentNullException(nameof(role));

            return Task.FromResult(role.Name);
        }

        public Task SetNormalizedRoleNameAsync(Role role, 
            string normalizedName, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (role == null) throw new ArgumentNullException(nameof(role));
            if (string.IsNullOrEmpty(normalizedName))
                throw new ArgumentException("normalizedName");

            return Task.CompletedTask;
        }

        public async Task<Role> FindByIdAsync(string roleId, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();

            if (int.TryParse(roleId, out int id))
            {
                return await db.Roles.SingleOrDefaultAsync(r => r.Id == id, 
                    cancellationToken);
            }
            else
            {
                return await Task.FromResult<Role>(null);
            }
        }

        public async Task<Role> FindByNameAsync(string normalizedRoleName, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (string.IsNullOrEmpty(normalizedRoleName))
                throw new ArgumentException("normalizedRoleName");

            Role role = await db.Roles.SingleOrDefaultAsync(
                r => r.Name.Equals(normalizedRoleName.ToLower()), 
                cancellationToken);

            return role;
        }
    }
}

(2) UserStore クラスへの追加

ロールを使えるようにするには、先に作成した UserStore クラス に機能の追加を行う必要があります。

具体的には、以下のように IUserRoleStore<User> インターフェースを継承し、AddToRoleAsunc, GetRolesAsync, IsInRoleAsync, RemoveFromRoleAsync メソッドを実装します。

using System.Collections.Generic;
using System.Linq;
using System.Reflection.Metadata.Ecma335;
using System.Threading;
using System.Threading.Tasks;

namespace MvcIdCustom.DAL
{
    public class UserStore : IUserStore<User>, 
                             IUserPasswordStore<User>, 
                             IQueryableUserStore<User>, 
                             IUserRoleStore<User>       // 追加
    {

        // ・・・中略・・・

        // Visual Studio のテンプレートで「個別のユーザーアカウント」を
        // 選んで自動生成される AspNetUserRoles テーブルは UserId と
        // RoleId の 2 つのフィールドのみが含まれ、それが連結主キーにな
        // っているので UserId と RoleId の組み合わせが重複することはない。

        // 一方、このプロジェクトで作った UserRole テーブルは Id, UserId,
        // RoleId の 3 つのフィールドを持ち Id が主キーになっている。なの
        // で UserId と RoleId の組み合わせが重複できてしまう。以下のコード
        // では重複設定できないように考えた(つもり)

        public async Task AddToRoleAsync(User user, string roleName, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw new ArgumentNullException(nameof(user));
            if (string.IsNullOrEmpty(roleName)) 
                throw new ArgumentException("roleName");

            // roleName に該当する Role オブジェクトを取得
            var role = await db.Roles.SingleOrDefaultAsync(
                             role => role.Name.Equals(roleName), 
                             cancellationToken);

            // roleName という名前のロールは存在しないので何もせず return
            if (role == null) return;

            // UserRole テーブルの UserId と RoleId が user.Id と role.Id
            // と一致する UserRole オブジェクトを取得
            var userRole = await db.UserRoles.SingleOrDefaultAsync(
                                 userRole => userRole.UserId.Equals(user.Id) && 
                                             userRole.RoleId.Equals(role.Id),
                                 cancellationToken);

            // user は roleName という名前のロールには既にアサイン済みなので
            // 何もせず return
            if (userRole != null) return;

            userRole = new UserRole
            {
                UserId = user.Id,
                RoleId = role.Id
            };

            db.UserRoles.Add(userRole);
            await db.SaveChangesAsync(cancellationToken);
        }

        public async Task RemoveFromRoleAsync(User user, string roleName, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw new ArgumentNullException(nameof(user));
            if (string.IsNullOrEmpty(roleName)) 
                throw new ArgumentException("roleName");

            // roleName に該当する Role オブジェクトを取得
            var role = await db.Roles.SingleOrDefaultAsync(
                             role => role.Name.Equals(roleName), 
                             cancellationToken);

            // roleName という名前のロールは存在しないので何もせず return
            if (role == null) return;

            // UserRole テーブルの UserId と RoleId が user.Id と role.Id
            // と一致する UserRole オブジェクトを取得
            var userRole = await db.UserRoles.SingleOrDefaultAsync(
                                userRole => userRole.UserId.Equals(user.Id) &&
                                            userRole.RoleId.Equals(role.Id), 
                                cancellationToken);

            // user は roleName という名前のロールにはアサインされてないので
            // 何もせず return
            if (userRole == null) return;

            db.UserRoles.Remove(userRole);
            await db.SaveChangesAsync(cancellationToken);
        }

        public async Task<IList<string>> GetRolesAsync(User user, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw 
                new ArgumentNullException(nameof(user));

            // UserRole テーブルの UserId フィールドが user.Id と一致する
            // UserRole オブジェクトを取得。Include を使ってナビゲーション
            // プロパティ経由 Role を紐づけているので Select メソッドで
            // ロール名のリストを取得できる
            var roleNames = await db.UserRoles
                .Include(userRole => userRole.Role)
                .Where(userRole => userRole.UserId.Equals(user.Id))
                .Select(userRole => userRole.Role.Name).ToListAsync();

            // 一件も見つからない時は null ではなく中身が空(Count がゼロ)
            // の List<string> オブジェクトを返す
            return roleNames;
        }

        public async Task<bool> IsInRoleAsync(User user, string roleName, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (user == null) throw new ArgumentNullException(nameof(user));
            if (string.IsNullOrEmpty(roleName)) 
                throw new ArgumentException("roleName");

            // UserRole テーブルの UserId フィールドが user.Id と一致する
            // UserRole オブジェクトを取得。Include を使ってナビゲーション
            // プロパティ経由 Role を紐づけているので Select メソッドで
            // ロール名のリストを取得できる
            var roleNames = await db.UserRoles
                .Include(userRole => userRole.Role)
                .Where(userRole => userRole.UserId.Equals(user.Id))
                .Select(userRole => userRole.Role.Name).ToListAsync();

            if (roleNames != null && roleNames.Count > 0)
            {
                // 引数 roleName は大文字に変換されて渡されるので ToUpper()
                // がないと true にならない。
                bool isInRole = roleNames.Any(
                                name => name.ToUpper() == roleName);
                return isInRole;
            }

            return false;
        }

        public async Task<IList<User>> GetUsersInRoleAsync(string roleName, 
            CancellationToken cancellationToken = default)
        {
            cancellationToken.ThrowIfCancellationRequested();
            if (string.IsNullOrEmpty(roleName)) 
                throw new ArgumentException("roleName");

            // roleName に該当する Role オブジェクトを取得
            var role = await db.Roles.SingleOrDefaultAsync(
                             role => role.Name.Equals(roleName),
                             cancellationToken);

            if (role != null)
            {
                var users = await db.UserRoles
                    .Include(userRole => userRole.User)
                    .Where(userRole => userRole.RoleId.Equals(role.Id))
                    .Select(userRole => userRole.User).ToListAsync();

                // 一件も見つからない時は null ではなく中身が空の
                // List<User> オブジェクトを返すはず
                return users;
            }

            return new List<User>();
        }
    }
}

(3) ロール管理画面の実装

先の記事「ASP.NET Identity のロール管理 (CORE)」と同等な方法で、ロールの表示・追加・変更・削除およびユーザーへのロールのアサイン・解除を行う機能を実装しました。

ただし、その記事に書いた「(1) ロールサービスの追加」は不要ですので注意してください。

実装した機能を使ってロールを作成し、ユーザーをロールにアサインした結果がこの記事の一番上にある画像です。

(4) /Account/AccessDenied の実装

コントローラーやアクションメソッドに [Authorize(Roles ="アドミ")] 属性を付与すると(ロール名は日本語 OK)、「アドミ」ロールを有してないユーザーがアクセスした場合は /Account/AccessDenied にリダイレクトされます。

/Account/AccessDenied を実装してないと、リダイレクトされた際 HTTP 404 Not Found エラーとなってしまいますので実装は必要です。

リダイレクトはフレームワークが自動的に行いますので、ユーザーにアクセス権限がないことを通知するだけの簡単なページを作れば OK です。


以上で完了です。

ロールによるアクセス制限は有効に働くようになります。ユーザーの持つロールに応じて /Account/Login および /Account/AccessDenied へのリダイレクト、許可されていればページの表示が期待通り動きます。

ユーザーがアサインされているロールの削除、ロールがアサインされているユーザーの削除も、内部で階層更新が行われているのか、問題なく実行でき、結果は期待通り SQL Server の当該テーブル反映されます。

その他、同じロール名の二重登録の防止機能が組み込まれており、例えば登録済みの Xxxxx というロール名を登録しようとすると Role name 'Xxxxx' is already taken. というエラーメッセージが出て登録に失敗します。

上記は上位レイヤーで処理されているようです。上記以外にもパスワードのハッシュや認証クッキーの発行など上位レイヤーの機能は多々あり、それを自力で実装するのは非常に難しいので、カスタム化する範囲は Identity Store レイヤー以下でないと手に負えないと思いました。

Tags: , , , ,

CORE

About this blog

2010年5月にこのブログを立ち上げました。その後 ブログ2 を追加し、ここは ASP.NET 関係のトピックス、ブログ2はそれ以外のトピックスに分けました。

Calendar

<<  January 2021  >>
MoTuWeThFrSaSu
28293031123
45678910
11121314151617
18192021222324
25262728293031
1234567

View posts in large calendar