WebSurfer's Home

トップ > Blog 1   |   Login
Filter by APML

ASP.NET Identity のユーザー管理 (CORE)

by WebSurfer 31. May 2020 11:45

ASP.NET Core Identity ベースのユーザー認証を実装した ASP.NET Core 3.1 MVC アプリで、管理者がユーザー情報の追加、変更、削除を行う機能を実装する例を書きます。

ユーザー管理画面

(登録ユーザーが多くなってくると、上の画面のような全員の一覧表示では管理が難しく、何らかの絞り込み機能が必要と思いますが、それは次の課題ということで・・・  手抜きですみません)

先の記事「ASP.NET Identity のユーザー管理 (MVC)」で .NET Framework 版の ASP.NET MVC5 の例を書きましたが、この記事は Core 3.1 版です。

ベースとなるのは、Visual Studio 2019 のテンプレートを使って、先の記事「ASP.NET Identity で MySQL 利用 (CORE 版)」のステップ (1)、(2) の手順で作成した ASP.NET Core 3.1 MVC アプリです。そのプロジェクトはデータベースに MySQL を使っていますが、この記事の手順は SQL Server でも同じです。

(1) Model の作成

プロジェクトのルート直下の Models フォルダに UserModel.cs というクラスファイルを追加し(名前は任意)、その中に以下のクラスを定義して使います。

using System;
using System.ComponentModel.DataAnnotations;

namespace MySQLIdentity.Models
{
    // UserController の Index, Details, Delete 用
    public class UserModel
    {
        public string Id { get; set; }

        [Display(Name = "ユーザー名")]
        public string UserName { get; set; }

        [Display(Name = "ハンドル名")]
        public string HandleName { get; set; }

        [Display(Name = "メールアドレス")]
        public string Email { get; set; }

        [Display(Name = "メール確認済")]
        public bool EmailConfirmed { get; set; }

        [Display(Name = "電話番号")]
        public string PhoneNumber { get; set; }

        [Display(Name = "電話番号確認済")]
        public bool PhoneNumberConfirmed { get; set; }

        [Display(Name = "二要素認証")]
        public bool TwoFactorEnabled { get; set; }

        [Display(Name = "ロック有効化")]
        public bool LockoutEnabled { get; set; }

        [Display(Name = "ロック終了日時")]
        public DateTimeOffset? LockoutEnd { get; set; }

        [Display(Name = "アクセス失敗数")]
        public int AccessFailedCount { get; set; }
    }

    // UserController の Create 用
    public class RegisterViewModel
    {
        [Required(ErrorMessage = "{0}は必須です。")]
        [EmailAddress]
        [Display(Name = "メールアドレス")]
        public string Email { get; set; }

        [Required(ErrorMessage = "{0}は必須です。")]
        [StringLength(100, ErrorMessage =
            "{0}は{2}から{1}文字の範囲で設定してください。",
            MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "パスワード")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "パスワード確認")]
        [Compare("Password", ErrorMessage = "確認パスワードが一致しません。")]
        public string ConfirmPassword { get; set; }
    }

    // UserController の Edit 用
    // 上の RegisterViewModel を使うとパスワードを入力しない場合は検証に
    // 通らない。違いは Password プロパティの [Required] を外しただけ
    public class EditViewModel
    {
        [Required(ErrorMessage = "{0}は必須です。")]
        [EmailAddress]
        [Display(Name = "メールアドレス")]
        public string Email { get; set; }

        [StringLength(100, ErrorMessage =
            "{0}は{2}から{1}文字の範囲で設定してください。",
            MinimumLength = 6)]
        [DataType(DataType.Password)]
        [Display(Name = "パスワード")]
        public string Password { get; set; }

        [DataType(DataType.Password)]
        [Display(Name = "パスワード確認")]
        [Compare("Password", ErrorMessage = "確認パスワードが一致しません。")]
        public string ConfirmPassword { get; set; }
    }
}

UserModel クラスは Index, Details, Delete 用の「ビューモデル」です。(「ビューモデル」は Controller から View にデータを渡すために使われます。詳しくは先の記事「ASP.NET MVC の Model」を見てください)

UserModel クラスに定義されているプロパティは、IdentityUser クラスを継承した MySQLIdentityUser カスタムクラスと同じです。(上に書いたプロジェクトの作成のステップ (1)、(2) で自動生成される Areas/Identity/Data フォルダの MySQLIdentityUser.cs ファイルに定義されています)

なので、MySQLIdentityUser カスタムクラスを「ビューモデル」として使うこともできます。しかし、(a) スキャフォールディング機能を使って View を生成できない(デザイナの「Razor ビューの追加」のダイアログで[モデルクラス(M):]の一覧に MySQLIdentityUser が現れない・・・理由不明)、(b) 表示名を自由に付けられない(プロパティ名と同じになる)・・・という理由で UserModel クラスを定義して使うことにしました。

そうすると、Controller で Linq To Entities を使って取得した MySQLIdentityUser オブジェクトを UserModel クラスに詰め替えなければならないという手間が増えますがやむを得ません。

詰め替えの手間を減らすため、先の記事「EDM にデータアノテーション属性を付与」に書いたようなメタデータクラスを作って対応することもトライしましたが、無駄な努力でした。

(2) Controller の作成

.NET Framework 版の ASP.NET MVC5 との大きな違いは、Core 版には組み込みの DI 機能があることでしょうか。今回のケースではコントローラーを初期化する時 UserManager<MySQLIdentityUser> オブジェクトを注入するようにしています。

コントローラーに UserManager<MySQLIdentityUser> オブジェクトへの参照を保持するフィールドを追加し、コンストラクタに UserManager<MySQLIdentityUser> オブジェクトへの参照を受け取る引数とそれをフィールドに代入するコードを書けば、あとは Areas/Identity フォルダに生成された IdentityHostingStartup.cs ファイル内の ConfigureServices メソッドで登録されたサービスが面倒を見てくれるようです。(詳しいメカニズムは調べ切れてませんが・・・)

ASP.NET Core Identity のデータベースに対し CRUD 操作を行うためのコントローラーのコードは以下の通りです。細かい注意点はコメントとして入れましたのでそちらを見てください。

using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

// 追加
using Microsoft.AspNetCore.Identity;
using MySQLIdentity.Areas.Identity.Data;
using Microsoft.EntityFrameworkCore;
using MySQLIdentity.Models;

namespace MySQLIdentity.Controllers
{
    public class UserController : Controller
    {
        // Register.cshtml.cs のコードにならって以下のフィールドと
        // コンストラクタを追加。
        // 組み込みの DI 機能を利用して、コントローラーを初期化する際
        // UserManager<MySQLIdentityUser> オブジェクトを注入するため
        private readonly UserManager<MySQLIdentityUser> _userManager;

        public UserController(UserManager<MySQLIdentityUser> userManager)
        {
            _userManager = userManager;
        }


        // GET: User/Index
        // Model は UserModel
        public async Task<IActionResult> Index()
        {
            var users = from user in _userManager.Users
                        orderby user.UserName
                        select new UserModel
                        {
                            Id = user.Id,
                            UserName = user.UserName,
                            HandleName = user.HandleName,
                            Email = user.Email,
                            EmailConfirmed = user.EmailConfirmed,
                            PhoneNumber = user.PhoneNumber,
                            PhoneNumberConfirmed = user.PhoneNumberConfirmed,
                            TwoFactorEnabled = user.TwoFactorEnabled,
                            LockoutEnabled = user.LockoutEnabled,
                            LockoutEnd = user.LockoutEnd,
                            AccessFailedCount = user.AccessFailedCount
                        };

            return View(await users.ToListAsync());
        }

        // GET: User/Details/5
        // Model は UserModel
        // Core には HttpNotFound, HttpStatusCodeResult は無いので
        // 代わりに NotFound() を使う
        // 引数の id は string 型にすること
        public async Task<IActionResult> Details(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var user = await _userManager.FindByIdAsync(id);

            if (user == null)
            {
                return NotFound();
            }

            var model = new UserModel
            {
                Id = user.Id,
                UserName = user.UserName,
                HandleName = user.HandleName,
                Email = user.Email,
                EmailConfirmed = user.EmailConfirmed,
                PhoneNumber = user.PhoneNumber,
                PhoneNumberConfirmed = user.PhoneNumberConfirmed,
                TwoFactorEnabled = user.TwoFactorEnabled,
                LockoutEnabled = user.LockoutEnabled,
                LockoutEnd = user.LockoutEnd,
                AccessFailedCount = user.AccessFailedCount
            };

            return View(model);
        }

        // GET: User/Create
        // Model は Model/UserModel.cs の RegisterViewModel
        public IActionResult Create()
        {
            return View();
        }

        // POST: User/Create
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create(
            [Bind("Email,Password,ConfirmPassword")] RegisterViewModel model)
        {
            // PasswordValidator による判定の結果は ModelState
            // には反映されないので注意(下記 result で判定)
            if (ModelState.IsValid)
            {
                // ユーザー入力のメールアドレスを UserName, Email,
                // HandleName プロパティに設定。さらに、ここで追加
                // したアカウントは Email Confirmation 無しで使用で
                // きるよう EmailConfirmed を true にする
                var user = new MySQLIdentityUser
                {
                    UserName = model.Email,
                    Email = model.Email,
                    HandleName = model.Email,
                    EmailConfirmed = true
                };

                // 上の user とユーザーが入力したパスワードで新規
                // ユーザーを作成・登録
                var result = await _userManager.
                    CreateAsync(user, model.Password);

                // ユーザー作成・登録の成否を result.Succeeded で
                // 判定。PasswordValidator の判定結果も result に
                // 反映される
                if (result.Succeeded)
                {
                    // 登録に成功したら User/Index にリダイレクト
                    return RedirectToAction("Index", "User");
                }
                // result.Succeeded が false の場合 ModelSate にエ
                // ラー情報を追加しないとエラーメッセージが出ない。
                // Register.cshtml.cs のものをコピー
                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

            // ユーザー登録に失敗した場合、登録画面を再描画
            return View(model);
        }

        // GET: User/Edit/5
        // ここで更新できるのは Email とパスワードのみ。
        // UseName は POST する際 Email と同じに設定する。
        // Model は Models/UserModel.cs の EditViewModel
        // 引数の id は string 型にすること
        public async Task<IActionResult> Edit(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var target = await _userManager.FindByIdAsync(id);

            if (target == null)
            {
                return NotFound();
            }

            EditViewModel model = new EditViewModel()
            {
                Email = target.Email
            };

            return View(model);
        }

        // POST: User/Edit/5
        // UserName をソルトに使っていてパスワードだけもしくは
        // UserName だけを更新するのは NG かと思っていたが問題
        // なかった。(実際どのように対処しているかは不明)
        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(string id, 
            [Bind("Email,Password,ConfirmPassword")] EditViewModel model)
        {
            if (id == null)
            {
                return NotFound();
            }

            if (ModelState.IsValid)
            {
                var target = await _userManager.FindByIdAsync(id);
                if (target == null)
                {
                    return NotFound();
                }

                target.Email = model.Email;
                target.UserName = model.Email;

                // 新パスワードを入力した場合はパスワードも更新する
                if (!string.IsNullOrEmpty(model.Password))
                {
                    // MVC5 と違って PasswordValidator プロパティはない
                    // PasswordValidators で IList<IPasswordValidator<TUser>>
                    // を取得できる。PasswordValidators[0] で検証可能
                    // (ホントにそれで良いのかどうかは分からないが)
                    // ValidateAsync メソッドの引数は MVC5 と違うので注意
                    var resultPassword = await _userManager.PasswordValidators[0].
                        ValidateAsync(_userManager, target, model.Password);

                    if (resultPassword.Succeeded)
                    {
                        // 検証 OK の場合、入力パスワードをハッシュ。
                        // HashPassword メソッドの引数は MVC5 とは異なる
                        var hashedPassword = _userManager.PasswordHasher.
                            HashPassword(target, model.Password);
                        target.PasswordHash = hashedPassword;
                    }
                    else
                    {
                        // 検証 NG の場合 ModelSate にエラー情報を
                        // 追加して編集画面を再描画
                        // Register.cshtml.cs のものをコピー
                        foreach (var error in resultPassword.Errors)
                        {
                            ModelState.AddModelError(string.Empty, error.Description);
                        }
                        return View(model);
                    }
                }

                var resultUpdate = await _userManager.UpdateAsync(target);

                if (resultUpdate.Succeeded)
                {
                    // 更新に成功したら User/Index にリダイレクト
                    return RedirectToAction("Index", "User");
                }
                // Register.cshtml.cs のものをコピー
                foreach (var error in resultUpdate.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

            // 更新に失敗した場合、編集画面を再描画
            return View(model);
        }

        // GET: User/Delete/5
        // Model は UserModel
        // 階層更新が行われているようでロールがアサインされている
        // ユーザーも削除可
        public async Task<IActionResult> Delete(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var u = await _userManager.FindByIdAsync(id);

            if (u == null)
            {
                return NotFound();
            }

            var model = new UserModel
            {
                Id = u.Id,
                UserName = u.UserName,
                HandleName = u.HandleName,
                Email = u.Email,
                EmailConfirmed = u.EmailConfirmed,
                PhoneNumber = u.PhoneNumber,
                PhoneNumberConfirmed = u.PhoneNumberConfirmed,
                TwoFactorEnabled = u.TwoFactorEnabled,
                LockoutEnabled = u.LockoutEnabled,
                AccessFailedCount = u.AccessFailedCount
            };

            return View(model);
        }

        // POST: User/Delete/5
        // 上の Delete(string id) と同シグネチャのメソッドは
        // 定義できないので、メソッド名を変えて、下のように
        // ActionName("Delete") を設定する
        [HttpPost, ActionName("Delete")]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> DeleteConfirmed(string id)
        {
            if (id == null)
            {
                return NotFound();
            }

            var target = await _userManager.FindByIdAsync(id);

            if (target == null)
            {
                return NotFound();
            }

            // ロールがアサインされているユーザーも以下の一行
            // で削除可能。内部で階層更新が行われているらしい。
            var result = await _userManager.DeleteAsync(target);

            if (result.Succeeded)
            {
                // 削除に成功したら User/Index にリダイレクト
                return RedirectToAction("Index", "User");
            }

            // Register.cshtml.cs のものをコピー
            foreach (var error in result.Errors)
            {
                ModelState.AddModelError(string.Empty, error.Description);
            }

            return View(target);
        }
    }
}

(3) View の作成

View は Controller のアクションメソッドを右クリックして[ビューの追加(D)...]からスキャフォールディング機能を使って生成します。

Razor ビューの追加

スキャフォールディング機能で自動生成された Index.cshtml のコードだけ以下に書いておきます。(生成後、一部手を加えなければならないところもありますので注意してください。この記事の例では ActionLink ヘルパーメソッドの第 3 引数を下のように修正しました)

@model IEnumerable<MySQLIdentity.Models.UserModel>

@{
    ViewData["Title"] = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

<h1>Index</h1>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.UserName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.HandleName)
            </th>
           <th>
                @Html.DisplayNameFor(model => model.PhoneNumber)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.UserName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.HandleName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.PhoneNumber)
            </td>
            <td>
                @Html.ActionLink("Edit", "Edit", new { id=item.Id }) |
                @Html.ActionLink("Details", "Details", new { id=item.Id }) |
                @Html.ActionLink("Delete", "Delete", new { id=item.Id })
            </td>
        </tr>
}
    </tbody>
</table>

この記事の一番上にある画像が Index.cshtml で描画されたものです。Details, Create, Edit, Delete 用の View もスキャフォールディング機能で生成できます。

Tags: , , ,

CORE

ASP.NET MVC の Model

by WebSurfer 29. May 2020 13:46

ASP.NET MVC で言う Model がどういうものかについて書いておきます。ネットの記事などで目にする一般的な MVC の Model の説明とは少々違っていて理解し難いかもしれませんので (実は自分がそうでした)。

ASP.NET MVC の Model

上の画像はマイクロソフト公式解説書「プログラミング Microsoft ASP.NET MVC」に記載されている図を借用したものです。ASP.NET MVC には「入力モデル」「ビューモデル」「ドメインモデル」という 3 種類の Model があることを示しています。それら各モデルが重なっている点にも注意してください。

重なっているのは、Model 自体は一つのクラスとして定義されており同じものですが、使われ方によって「入力モデル」「ビューモデル」「ドメインモデル」というように役割が変わるということと理解すれば良いと思います。

一方、ネットなどでよく目にする一般的な MVC の Model の説明は、例えば Wikipedia の記事 Model View Controller にあるように「アプリケーションデータ、ビジネスルール、ロジック、関数」という役割を持ち、クライアント側から見て View と Controller の背後にあるという構成で説明されていることが多いと思います。

ASP.NET MVC の 3 種類の Model のうち、「ドメインモデル」が上で言う「アプリケーションデータ、ビジネスルール、ロジック、関数」に近いものです。

「ビューモデル」は Controller から View にデータを渡すために使われます。「入力モデル」はクライアントから送信されてくるデータを Controller のアクションメソッドに渡す時に使われます。

以下に説明のための具体例を書きます。

まず、プロジェクトの Models フォルダに���以下の Student クラスの定義(即ち、Model の定義)を含むクラスファイルがあるとします。

using System;
using System.Collections.Generic;

namespace Mvc5App.Models
{
    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        public DateTime EnrollmentDate { get; set; }
    }
}

さらに、データーベースに Student テーブルがあって、それを Entity Framework を利用して編集・更新を行うための Controller と View を作るとします。

編集用の画面を表示するための Controller のアクションメソッドは以下のようになります。

public async Task<ActionResult> Edit(int? id)
{
    Student student = await db.Students.FindAsync(id);

    return View(student);
}

db.Students.FindAsync(id) の db は DbContext クラスを継承するコンテキストクラスで、Entity Framework を利用してデータベースへのクエリの実行などを行うものです。このコードでは、指定された id のレコードをデータベースから取得して Student オブジェクトを作っています。その部分は上の図で「ドメインモデル」に該当します。

return View(student) で View に Student オブジェクトを渡していますが、それが「ビューモデル」に該当します。

アクションメソッドに対応する View (Edit.cshtml) のコードの一行目に @model Mvc5App.Models.Student と書けば、View の中では Model プロパティで Student オブジェクトを取得できます。また、EditorFor などの Html ヘルパーでは、例えばその引数を model => model.LastName とすれば model に Student オブジェクトが渡されます。

View によって html ソースが生成されクライアント (ブラウザ) に送信されます。ユーザーがブラウザに表示されたテキストボックスの内容を編集後、ボタンクリック操作などでデータを送信するとサーバー側ではモデルバインディングという操作が行われます。

ブラウザから送信されてきたデータを受け取ってデーターベースを更新する Controller のアクションメソッドは以下のようになっています。モデルバインディングというのは、送信されてきたデータで Student オブジェクトを作成し、それをアクションメソッドの引数 student に渡す操作ですが、その時使うのが上の図の「入力モデル」になります。

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(Student student)
{
    if (ModelState.IsValid)
    {
        db.Entry(student).State = EntityState.Modified;
        await db.SaveChangesAsync();
        return RedirectToAction("Index");
    }
    return View(student);
}

ModelState.IsValid が true の場合(送信データの検証結果が OK の場合)、if 文の中の最初の 2 行で既存のレコードが更新されます。その部分も上の図で「ドメインモデル」に該当します。

ASP,NET MVC アプリを作っているときに目立つのが「ビューモデル」と「入力モデル」と思いますが、それらは上に紹介した Wikipedia の記事にあるような一般的な MVC の Model の説明の機能「アプリケーションデータ、ビジネスルール、ロジック、関数」は持たないので、説明と違うということが混乱を招くところと思います。

Tags: , ,

MVC

プロファイル情報の追加 (.NET 版)

by WebSurfer 26. May 2020 16:23

.NET Framework 版の ASP.NET MVC5 アプリに ASP.NET Identity を利用したユーザー認証を実装して、プロファイル情報としてハンドル名を追加し、ログイン時にページのヘッダ右上にハンドル名(デフォルトはメールアドレス)を表示する方法を書きます。

ハンドル名を表示

先の記事「プロファイル情報の追加 (CORE 版)」の .NET Framework 版です。CORE 版とは基本的なところでの大きな差はありませんが、細かいところでいろいろ違うので備忘録として残しておくことにしました。

(1) プロジェクトの作成

以下の説明は、Visual Studio 2019 を使って Mvc5ProfileInfo という名前で作成した ASP.NET MVC5 アプリのプロジェクトをベースしています。

プロジェクトの作成

デフォルトでは認証は「認証なし」になっていますが、それを「個別のユーザーアカウント」に変更します。それによって ASP.NET Identity を利用したユーザー認証のためのソースコードが一式自動生成されます。

(2) HandleName プロパティの追加

Models/IdentityModels.cs ファイルに IdentityUser を継承した ApplicationUser クラスが定義されています。それに HandleName プロパティを追加します。以下の画像の赤枠部分がそれです。

HandleName プロパティの追加

そのあと Migration 操作を行うと Entity Framework Code First の機能によりデータベースには HandleName フィールドが作られます。HandleName フィールドはデータアノテーション属性の Required により NULL 不可に、StringLength の引数 256 により nvarchar(256) になります。

(3) Enable-Migrations / Add-Migration

Visual Studio のパッケージマネージャーコンソールで Enable-Migrations コマンドを実行します。成功するとプロジェクトのルート直下に Migrations フォルダが生成され、その中に Configuration.cs という名前のクラスファイルが生成されます。

次に、Add-Migration Initial コマンドを実行します(Initial という名前は任意です)。成功すると Migrations フォルダに xxxxx_Initial.cs という名前のクラスファイルが生成されます。(xxxxx は作成日時)

Initial.cs

その中に、dbo.AspNetUsers テーブルを SQL Server に生成するコードがあります。上の画像の赤枠のコードを見てください。ステップ (2) で HandleName プロパティに付与した属性に従って HandleName というフィールドを NULL 不可、nvarchar(256) で生成するようになっています。

(注: この記事ではプロジェクト作成直後のデータベースには何もない状態から Migration 関係の操作を行っています。データーベースがすでに生成済みで、それに HandleName を追加する場合とは手順が違うので注意してください)

(4) Update-Database

Visual Studio のパッケージマネージャーコンソールで Update-Database コマンドを実行します。成功すると ASP.NET Identity 用のデータベースが Entity Framework Code First の機能によって追加されます。

ASP.NET Identity 用のデータベース

この記事では Visual Studio でプロジェクトを作成したときに自動生成された web.config をそのまま使っています。その中に "DefaultConnection" という名前で LocalDB に接続する接続文字列が定義されており、aspnet-Mvc5ProfileInfo-20200526103410.mdf という名前(数字はプロジェクトの作成日時)の .mdf ファイルを動的に LocalDB にアタッチして使うように設定されています。

Models/IdentityModels.cs ファイルには IdentityDbContext<ApplicationUser> を継承したコンテキストクラス ApplicationDbContext が定義されており、web.config の接続文字列 "DefaultConnection" を使うように設定されています。

Update-Database コマンドによって、web.config の接続文字列で LocalDB に接続し、指定された名前のデータベースを LocalDB に作成し、ステップ (3) で生成されたクラスファイル xxxxx_Initial.cs に従って ASP.NET Identity 用のテーブルを生成します。

その結果が上の画像です。aspnet-Mvc5ProfileInfo-20200526103410 という名前のデータベースが作成され、その中の dbo.AspNetUsers テーブルにステップ (2) で HandleName プロパティを追加したとおり、HandleName フィールドが NULL 不可 nvarchar(256) で生成されています。

(5) Register アクションメソッドの修正

Controllers/AccountController.cs の Register アクションメソッドに以下の画像の赤枠のコードを追加します。登録時にハンドル名を自動的に Email と同じになるようにするものです。

Register アクションメソッドの修正

ステップ (4) の画像の通り dbo.AspNetUsers テーブルの HandleName フィールドは NULL 不可になっています。そのため、新規ユーザーの登録を行う際 HandleName を入力しないと CreateAsync でエラーとなります。なので、初回登録時はとりあえずハンドル名は Email と同じになるようにしました。

登録時にユーザーにハンドル名を決めて入力してもらうようにもできますが、そういう手間を増やすと嫌がられそうですし、ユーザーによってはハンドル名は Email と同じでよいという人もいるでしょうから。

ここまで実装できれば、アプリケーションを実行してユーザー登録が可能になり、登録操作を行うと dbo.AspNetUsers テーブルの HandleName フィールドにはメールアドレスと同じ文字列が設定されているはずです。

(6) ハンドル名の変更機能を実装

ログイン後、ページのヘッダの右上のユーザー名(デフォルトで Email)をクリックすると Manage/Index ページに遷移しますが、そこからさらにリンクをたどって別ページに飛んで、そこでユーザーがハンドル名を任意に変更できるようにします。

ハンドル名の変更は、Manage コントローラーに ChangeHandleName という名前のアクションメソッドを追加しそこで行うようにします。

まず、Manage/Index ページのビュー Views/Manage/Index.cshtml に ChangeHandleName アクションメソッドへのリンクを追加します。

ChangeHandleName へのリンク追加

変更が完了した際に Manage/Index ページに表示するためのメッセージを追加します。Controllers/ManageController.cs ファイルを開いて以下の画像の赤枠のコードを追加します(2 箇所)。

メッセージを追加

メッセージを追加

Models/ManageViewModels.cs に ChangeHandleName アクションメソッドとビューの間でデータをやり取りするための ChangeHandleNameViewModel クラスを追加します。入力できるハンドル名の長さはとりあえず 30 文字に制限してみました。

// HandleName 変更用に追加
public class ChangeHandleNameViewModel
{
    [Required]
    [Display(Name = "旧ハンドル名")]
    public string OldHandleName { get; set; }

    [Required(ErrorMessage = "{0}は必須")]
    [StringLength(30, ErrorMessage = "{0} は {1} 文字以内")]
    [Display(Name = "新ハンドル名")]
    public string NewHandleName { get; set; }
}

Views/Manage/Index.cshtml に ChangeHandleName アクションメソッドのコードを追加します。旧ハンドル名を入力できるようになっていたり、いきなり例外をスローするところがちょっと乱暴かもしれませんが、検証用ということで・・・

// GET: /Manage/ChangeHandleName
public async Task<ActionResult> ChangeHandleName()
{
    var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
    if (user == null)
    {
        throw new InvalidOperationException(
            "ログイン中のユーザー情報が削除されました。");
    }

    var model = new ChangeHandleNameViewModel();
    model.OldHandleName = user.HandleName;
    return View(model);
}

// POST: /Manage/ChangeHandleName
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> ChangeHandleName(ChangeHandleNameViewModel model)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    var user = await UserManager.FindByIdAsync(User.Identity.GetUserId());
    if (user == null)
    {
        throw new InvalidOperationException(
            "ログイン中のユーザー情報が削除されました。");
    }

    if (model.OldHandleName != user.HandleName)
    {
        throw new InvalidOperationException(
            "DB に既存のハンドル名と旧ハンドル名が一致しません。");
    }

    user.HandleName = model.NewHandleName;
    var result = await UserManager.UpdateAsync(user);
    if (result.Succeeded)
    {
        return RedirectToAction("Index", 
            new { Message = ManageMessageId.ChangeHandleNameSuccess });
    }
    AddErrors(result);
    return View(model);
}

アクションメソッドを追加したら、スキャフォールディングのデザイナで[テンプレート(T)]を Edit とし[モデルクラス(M)]を上に定義した ChangeHandleNameViewModel としてビューを追加します。コードは自動生成されたものがほぼそのまま使えますのでこの記事に書くのは割愛します。

(7) ハンドル名の変更操作

ユーザーがログインするとページのヘッダの右上に Hello xxxxx! と表示され、それが Manage/Index ページへのリンクになります。それをクリックして Manage/Index ページを表示し、その中のリンク[ハンドル名の変更]をクリックするとすると Manage/ChangeHandleName ページに遷移します。

ChangeHandleName ページ

上の[新ハンドル名]テキストボックスに新しいハンドル名を入力し[Save]ボタンをクリックするとデータベースの HandleName フィールドが更新され、その後 Manage/Index ページにリダイレクトされます。

Manage/Index ページにリダイレクト

赤枠で示した部分にステップ (6) で追加したメッセージが表示されているところに注目してください。

(8) ClaimsIdentity へ追加

ページのヘッダの右上に Hello xxxxx! と表示される xxxxx にはデフォルトではメールアドレスが表示されますが(ステップ (7) の画像参照)、それをハンドル名に変更します。結果、この記事の一番上の画像のようになります。

プロファイル情報を Claim として ClaimsIdentity オブジェクトに追加し、拡張メソッドを使って ClaimsIdentity オブジェクトからハンドル名を取得・表示するようにしています。

プロジェクト作成時に自動生成された Models/IdentityModels.cs ファイルの ApplicationUser クラスにそのコードを書く場所がコメントで「ここにカスタム ユーザー クレームを追加します」と示されているので、そこに追加します。以下の画像の上の赤枠部分の通りです。

カスタムユーザークレーム

ClaimsIdentity オブジェクトからハンドル名を取得する拡張メソッドもそこに書いておきます。画像の下の赤枠のコードがそれです。

画像には表示されていませんが、using 句で名前空間 System.Linq と System.Security.Principal を取り込むようにしてください。

拡張メソッドは名前空間をインポートすればスコープの中に取り込むことができます。この記事の一番上の画像のようにレイアウトページの右上に表示する場合は Views/Shared/_LoginPartial.schtml に名前空間 Mvc5ProfileInfo.Models を取り込んで User.Identity.GetHandleName() というコードで取得します。

ClaimsIdentity オブジェクトにハンドル名を追加すると、認証クッキーにハンドル名が含まれるようになります。認証クッキーからハンドル名を取得できれば、毎回データベースにクエリを投げて取得するより負荷は軽い(であろう)というのが ClaimsIdentity を使う理由です。

Tags: , , ,

MVC

About this blog

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

Calendar

<<  September 2020  >>
MoTuWeThFrSaSu
31123456
78910111213
14151617181920
21222324252627
2829301234
567891011

View posts in large calendar