WebSurfer's Home

Filter by APML

ASP.NET Core Identity をスキャフォールディングで実装

by WebSurfer 14. February 2024 14:00

Visual Studio Community 2022 のテンプレート「ASP.NET Core Web アプリ (Model-View-Controller)」を使って認証なしで作成した .NET 8.0 の ASP.NET Core MVC プロジェクトに、ASP.NET Core Identity を実装する方法を書きます。

認証なしで ASP.NET Core MVC プロジェクト作成

上の画像で[認証の種類(A)]を「個別のアカウント」に設定してプロジェクトを作成するとユーザー認証に必要な ASP.NET Core Identity が実装されますが、Razor クラス ライブラリ (RCL) として実装されるためソースコードはプロジェクトに含まれません。なので、例えばログイン・登録ページを書き換えたい場合は、スキャフォールディング機能を利用してログイン・登録ページのソースコードを生成し、それに手を加えるということになります。

書き換えるまでは必要なくても単純に日本語化したいページはログイン・登録ページ以外にも多々あるので、この際[認証の種類(A)]を「なし」に設定してプロジェクトを作成し、それにスキャフォールディング機能を利用して ASP.NET Core Identity 関係のソースコードを一式すべて実装する方が良さそうです。

基本的なことは Microsoft のドキュメント「ASP.NET Core プロジェクトでの Identity のスキャフォールディング」の「MVC プロジェクトに既存の認可なしで Identity をスキャフォールディングする」のセクションに書いてありますが、それだけでは情報不足だと思いますので、以下に画像を加えて詳しく書いておきます。

(1) Microsoft.VisualStudio.Web.CodeGeneration.Design

NuGet から Microsoft.VisualStudio.Web.CodeGeneration.Design をインストールします。 この操作をスキップしても、この後のステップで自動的に追加されますが、 自分でバージョンを確認してからインストールするのが良さそうです。

(2) ASP.NET Core Identity のソースコード実装

スキャフォールディング機能を利用して ASP.NET Core Identity のソースコードを実装します。 具体的な方法は以下の通りです。

  1. ソリューションエクスプローラーでプロジェクトノードを右クリックすると表示されるメニューで[追加(D)]⇒[新規スキャフォールディングアイテム(F)...]を選択。 「新規スキャフォールディングアイテムの追加」ダイアログが表示されるので ID を選択して[追加]ボタンをクリック。

    新規スキャフォールディングアイテムの追加
  2. 「ID の追加」ダイアログが表示されるので[すべてのファイルをオーバーライド]にチェックを入れます。

    ID の追加
  3. DbContext クラス名は、上の画像の + のアイコンをクリックして出てくる「データコンテキストの追加」ダイアログでは、 例えばプロジェクト名が MvcNet8App2 の場合、下の画像のように MvcNet8App2.Data.MvcNet8App2Context というプロジェクト名が反映された名前になります。 この MvcNet8App2Context を ApplicationDbContext に変更します。

    データコンテキストの追加

    スキャフォールディング機能で自動生成される ASP.NET Core Identity 関係のコードで ApplicationDbContext という名前が使われていて、ApplicationDbContext でないとエラーになるというケースが過去にありました。 改善されているかもしれませんが(未確認です)、変更した方が無難だと思います。名前の変更は必ず「データコンテキストの追加」ダイアログ上で行う必要がありますので注意してください。
  4. 上のステップで名前を変更後[追加]ボタンをクリックすると結果が以下のように反映されます。 この時点でデーターベースプロバイダーには SQL Server が選択され、この後の操作で Program.cs に SQL Server を使う設定がなされます。

    DbContext クラス
  5. ユーザークラス名は、上の画像の + のアイコンをクリックして出てくる「ユーザークラスの追加」ダイアログでは MvcNet8App2User というプロジェクト名が反映された名前になります。 これを ApplicationUser に変更し、さらに、DbContext クラスと同じフォルダ Data にユーザークラスのファイルが作られるよう、同じ名前空間 MvcNet8App2.Data を付与します。

    ユーザークラスの追加
  6. 上のステップで名前を変更後[追加]ボタンをクリックすると結果が以下のように反映されます。確認して[追加]ボタンをクリックします。

    ユーザークラス
  7. 成功すると、プロジェクトルート直下に Areas フォルダが生成され、その中に ASP.NET Core Identity 関係のファイルが一式生成されます。 加えて、appsettings.json ファイルに接続文字列が追加され、Views/Shared フォルダに _LoginPartial.cshtml が追加され、 Program.cs に ApplicationDbContext と ApplicationUser のインスタンスを DI によって取得できるようにするためのコードが追加されます。

(3) ソリューションのリビルド

ソリューションをリビルドし、エラー無く完了することを確認してください。

ちなみに、前のバージョン .NET 7.0 の時は Pages\Account\Logout.cshtml と Pages\Account\Manage\_Layout.cshtml に NULL 許容参照型に対応していないコードが含まれていて警告が出ましたが、.NET 8.0 では警告が出ないように改善されていました。バージョンアップで進歩しているようです。

(4) レイアウトページの修正

スキャフォールディングで自動生成された Areas/Identity/Page/Account/Manage/ フォルダの _Layout.cshtml のコードで Layout = "/Areas/Identity/Pages/_Layout.cshtml"; を Layout = "/Views/Shared/_Layout.cshtml"; に変更します。

(5) LoginPartial を追加

Views/Shared/_Layout.cshtml に <partial name="_LoginPartial" /> を追加します。

LoginPartial を追加

これによりページの右上に下の画像の赤枠部分のように登録・ログイン・ログアウト操作のためにリンクが表示されるようになります。

LoginPartial の表示

(6) Program.cs の修正

Program.cs に builder.Services.AddRazorPages(); と app.MapRazorPages(); を追加します。 これがないとメニューバーの上のステップ (5) 画像の Register, Login をクリックしても Razor Page で作られた Register, Login に遷移しません。

(7) Add-Migration の実行

パッケージマネージャーコンソールから Add-Migration CreateIdentitySchema を実行します。CreateIdentitySchema という名前は任意に付けられます。

成功すると Migrations フォルダが生成され、その中に 20240115003149_CreateIdentitySchema.cs と ApplicationDbContextModelSnapshot.cs が生成されます。20240115003149 は Add-Migration を実行した日時、CreateIdentitySchema は上のコマンドで指定した名前です。

(8) Update-Database の実行

次に Update-Database を実行し、Entity Framework Code First の機能を利用してデータベース / テーブルを生成します。成功すると、appsettings.json に指定した接続文字列の Database に設定された名前のデータベースと必要なテーブル一式が生成されます。

データベース / テーブル

(9) ユーザー登録

以上で完了です。アプリを起動し Register ページでユーザー登録すれば上のステップ (8) で作成したデータベース / テーブルに反映されます。

Email Confirmation がデフォルトで有効になっており、手順に表示される Register confirmation ページのリンク "Click here to confirm your account" をクリックしないとアカウントが有効にならないので注意してください。

Register confirmation ページ

Tags: , , ,

Authentication

ASP.NET Core MVC で Bootstrap5 Modal の利用

by WebSurfer 11. November 2023 10:46

Visual Studio 2022 を使って作成する ASP.NET Core MVC アプリで、データベースのテーブル一覧を表示し、その中の項目を選択すると、その詳細を Bootstrap5 の Modal を利用して表示する方法を書きます。

Bootstrap Modal に詳細を表示

この記事ではターゲットフレームワークを .NET 7.0 としています。.NET 5.0 以前をターゲットフレームワークとして作成したプロジェクトでは Bootstrap のバージョンが異なるので注意してください。ちなみに、.NET Core 3.1 ~ 5.0 は Bootstrap v4.3.1、.NET 6.0 ~ 8.0 は Bootstrasp v5.1 です。

この記事のベースとしたアプリは、Microsoft のチュートリアル「CRUD 機能を実装する」です。そのアプリの Student テーブルのレコード一覧を表示する Index ページと各レコードの詳細を表示する Details ページを使います。

チュートリアルでは、Index ページに一覧表示されている各項目の右横のアクションリンク Details をクリックすると、別ページ Details に遷移し、遷移した Details ページ上で選択した項目の詳細を表示するようになっています。チュートリアルの「Details ビューに登録を追加する」セクションの下の方の画像を見てください。

それを、別ページ Details に遷移しないで、この記事の上の画像のように Index ページ上で Bootstrap Modal 内に詳細を表示するようにしてみます。

Bootstrap については、Visual Studio 2022 のテンプレートで、ターゲットフレームワーク .NET 7.0 として作成した ASP.NET Core MVC アプリのプロジェクトに Bootstrap.js, Boorstrap.css v5.1.0 が含まれています。Bootstrap Modal を使うために必要なものもそれらの中に含まれているので何も追加する必要はありません。

レイアウトページ _Layout.cshtml にはプロジェクトの Bootstrap.js, Boorstrap.css を参照する link タグ、script タグが含まれていますので、スキャフォールディングの際 _Layout.cshtml を使用するように設定すれば、それらが参照されるようになります。

Bootstrap5 Modal の詳しい説明は Bootstrap のドキュメント「Modal (モーダル)」にあります。普通の html + css + javascript のページに Bootstrap5 Modal を組み込むなら、そのドキュメントから十分な情報が得られると思います。

ASP.NET Core MVC アプリに、Bootstrap5 Modal を組み込んで、Index ページにのアクションリンクをクリックして詳細データを取得し、Index ページ上で Modal に表示するにはどうしたらいいかというのががこの記事のポイントで、それを以下に述べます。

まずコントローラーのアクションメソッドですが、Index はチュートリアルのものと全く同じ、Details も最後の一行で部分ビューを返すように変更する以外はチュートリアルのものと同じです。

コードは以下の通りとなります。この記事ではアクションメソッドの名前を Index3, Details3 としています。

using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using MvcNet7App.Data;
using MvcNet7App.Models;

namespace MvcNet7App.Controllers
{
    public class StudentsController : Controller
    {
        private readonly SchoolContext _context;

        public StudentsController(SchoolContext context)
        {
            _context = context;
        }

        // Student のレコード一覧を取得しビューに渡す
        public async Task<IActionResult> Index3()
        {
            return _context.Students != null ?
                   View(await _context.Students.ToListAsync()) :
                   Problem("Entity set 'SchoolContext.Students'  is null.");
        }

        // 詳細を表示する部分ビュー用のアクションメソッド
        public async Task<IActionResult> Details3(int? id)
        {
            if (id == null || _context.Students == null)
            {
                return NotFound();
            }

            //var student = await _context.Students
            //    .FirstOrDefaultAsync(m => m.ID == id);
            var student = await _context.Students
                                        .Include(s => s.Enrollments)
                                            .ThenInclude(e => e.Course)
                                        .AsNoTracking()
                                        .FirstOrDefaultAsync(m => m.ID == id);

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

            // 部分ビューを返すように変更
            return PartialView(student);
        }
    }
}

次に、上の Index アクションメソッドに対応するビューですが、スキャフォールディングで自動生成されたコードに (1) Modal 部分を実装、(2) Details アクションリンクを JavaScript のメソッドを呼び出すよう変更、(3) fetch API でサーバーに要求を出し、応答として返される部分ビューを Modal 内の div 要素に書き込んだ後 Modal を表示する JavaScript のメソッドを追加します。

コードは以下の通りです。Modal 部分は Bootstrap 5 のドキュメント「Modal (モーダル)」の「Static backdrop」セクションのサンプルコードをコピペし、若干修正をして利用しています。詳細を書き込む div 要素に id="details" を追加したこと、デフォルトの "medium" サイズでは幅が足らないので CSS に modal-lg を追加したこと、Understood ボタンは削除したことが主な違いです。

@model IEnumerable<MvcNet7App.Models.Student>

@{
    ViewData["Title"] = "Index3";
}

<h1>Index3</h1>

<!-- (1) Modal 部分を実装 -->
<div class="modal fade"
     id="staticBackdrop"
     data-bs-backdrop="static"
     data-bs-keyboard="false" tabindex="-1"
     aria-labelledby="staticBackdropLabel"
     aria-hidden="true">
    <div class="modal-dialog modal-lg">
        <div class="modal-content">
            <div class="modal-header">
                <h5 class="modal-title"
                    id="staticBackdropLabel">
                    Student Details
                </h5>
                <button type="button"
                        class="btn-close"
                        data-bs-dismiss="modal"
                        aria-label="Close">
                </button>
            </div>
            <div class="modal-body" id="details">
                ...
            </div>
            <div class="modal-footer">
                <button type="button"
                        class="btn btn-primary"
                        data-bs-dismiss="modal">
                    Close
                </button>
            </div>
        </div>
    </div>
</div>

<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.LastName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.FirstMidName)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>
                    @Html.DisplayFor(modelItem => item.LastName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.FirstMidName)
                </td>
                <td>
                    @* (2) クリックで JavaScript の showDetails
                    を呼び出す。引数には Student の ID を渡す *@ 
                    <a href="javascript:showDetails(@item.ID)">
                        Details
                    </a>
                </td>
            </tr>
        }
    </tbody>
</table>

@section Scripts {
    <script type="text/javascript">

        // (3) fetch API でサーバーに要求を出し、応答として返される
        // 部分ビューを Modal 内の div 要素に書き込んだ、後 Modal を
        // 表示する

        // Modal を表示するためのヘルパーメソッド
        const showBootstrapModal = () => {
            // staticBackdrop は Modal の div 要素の id
            let divModal = document.getElementById('staticBackdrop');
            let myModal = new bootstrap.Modal(divModal);
            myModal.show();
        }

        const showDetails = async (id) => {
            // 部分ビュー用アクションメソッドの url
            // id は Student テーブルのレコードの ID
            const url = "/Students/Details3/" + id;

            // Student 詳細を表示する Modal 内の div 要素
            const resultDiv = document.getElementById('details');

            const response = await fetch(url);
            if (response.ok) {
                // 部分ビューのテキストを取得
                const text = await response.text();

                // テキストを Modal 内の div 要素に書き込み
                resultDiv.innerHTML = text;

                // Modal を表示
                showBootstrapModal();
            }            
        }

    </script>
}

最後に、Details アクションメソッドに対応する部分ビューですが、以下の通りとなっています。チュートリアルで自動生成されたコードから部分ビューに不要な部分を削除しただけです。

@model MvcNet7App.Models.Student

<div>
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.LastName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.LastName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.FirstMidName)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.FirstMidName)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.EnrollmentDate)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.EnrollmentDate)
        </dd>

        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Enrollments)
        </dt>
        <dd class="col-sm-10">
            <table class="table">
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </dd>
    </dl>
</div>

Tags: , , ,

CORE

ASP.NET Core のカスタムクラスから HttpContext にアクセス

by WebSurfer 19. September 2023 14:59

ASP.NET Core Web アプリ内のカスタムクラスから HttpContext にアクセスする方法を備忘録として書いておきます。下の画像は、カスタムクラスから HttpContext にアクセスし、ログイン中のユーザー名を取得して表示したものです。赤枠の部分がそれです。

ログイン中のユーザー名を取得

その方法は Microsoft のドキュメント「ASP.NET Core で HttpContext にアクセスする」の中の「カスタム コンポーネントから HttpContext を使用する」を見ればすぐ分かるのですが、どこに書いてあったか忘れて探し回ることがないように自分のブログに書いておくことにしました。

基本的にどうするかというと、Visual Studio のテンプレートを使って作成する ASP.NET Core アプリには Dependency Injection (DI) 機能が組み込まれていますのでそれを利用します。

まず、DI コンテナに IHttpContextAccessor サービスと、それを利用するカスタムクラスを登録します。具体的には、Program.cs に以下のようにコードを追加します。

using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.EntityFrameworkCore;
using MvcNet7App.Data;
using MvcNet7App.Libraries;

var builder = WebApplication.CreateBuilder(args);

// ・・・中略・・・

// DI コンテナに IHttpContextAccessor サービスを登録
builder.Services.AddHttpContextAccessor();

// それを利用するカスタムクラスを登録
builder.Services.AddTransient<IUserRepository, UserRepository>();

// ・・・後略・・・

カスタムクラス UserRepository の例は以下の通りです。DI 機能によって、コンストラクタ経由で IHttpContextAccessor サービスを inject できるようにしています。LogCurrentUser メソッドは、inject された IHttpContextAccessor サービスから HttpContext にアクセスし、ログイン中のユーザー名を取得して戻り値として返します。

namespace MvcNet7App.Libraries
{
    public class UserRepository : IUserRepository
    {
        // DI でコンストラクタの引数経由 IHttpContextAccessor 
        // サービスを inject できるよう設定
        private readonly IHttpContextAccessor _httpContextAccessor;

        public UserRepository(IHttpContextAccessor httpContextAccessor)
        {
            _httpContextAccessor = httpContextAccessor;
        }

        public string? LogCurrentUser()
        {
            // ログイン中のユーザー名を取得
            var username = _httpContextAccessor.HttpContext?
                           .User.Identity?.Name;
            return username;
        }
    }

    public interface IUserRepository
    {
        public string? LogCurrentUser();
    }
}

上のカスタムクラスを利用する場合は、組み込みの DI 機能を使ってカスタムクラスのインスタンスが渡されるようにします。例えば、MVC の Controller の場合、以下のようにコンストラクタの引数を経由してカスタムクラスのインスタンスを渡しています。

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using MvcNet7App.Libraries;
using MvcNet7App.Models;
using System.Diagnostics;

namespace MvcNet7App.Controllers
{
    public class HomeController : Controller
    {
        private readonly ILogger<HomeController> _logger;

        // 組み込みの DI 機能によりコンストラクタの引数経由で
        // カスタムクラスのインスタンスが渡される
        private readonly Libraries.IUserRepository _user;

        public HomeController(ILogger<HomeController> logger, 
                              IUserRepository user)
        {
            _logger = logger;
            _user = user;
        }

        public IActionResult Index()
        {
            ViewBag.Username = _user.LogCurrentUser();

            return View();
        }

        // ・・・中略・・・
    }
}

ユーザーが上の Home/Index を要求すると、ASP.NET は HomeController のインスタンスを生成して要求を処理しますが、その際 DI コンテナに登録された UserRepository クラスと IHttpContextAccessor サービスのインスタンスは自動的・連鎖的に生成され、コンストラクタの引数経由で inject されます。

なので、Index アクションメソッドで、inject された UserRepository クラスの LogCurrentUser メソッドを使ってログイン中のユーザー名を取得できます。

取得したユーザー名を ViewBag を使って View に渡し、ブラウザ上に表示したのがこの記事の一番上の画像です。赤枠の中の surfer@mail.example.com がそれです。


なお、Controller や View から HttpContext にアクセスする場合は、DI 機能などは使う必要はなく、もっと簡単にできます。詳しくは「ASP.NET Core の HttpContext にアクセスする」を見てください。

Tags: , ,

CORE

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。ブログ2はそれ以外の日々の出来事などのトピックスになっています。

Calendar

<<  January 2026  >>
MoTuWeThFrSaSu
2930311234
567891011
12131415161718
19202122232425
2627282930311
2345678

View posts in large calendar