WebSurfer's Home

トップ > Blog 1   |   ログイン
APMLフィルター

参照型と ASP.NET Core の検証

by WebSurfer 2024年2月23日 17:49

.NET 6.0 以降の ASP.NET Core Web アプリで、コンテキストクラスとエンティティクラスを使って SQL Server などのデータベースのテーブルの CRUD 操作を行う際、エンティティクラス内の参照型のプロパティを null 許容にしておかないと検証に通らず、Create や Edit 操作に失敗するという話を書きます。特に下の画像のようなナビゲーションプロパティが要注意です。

検証結果が NG

また同じ問題にハマって時間を無駄にしないよう、どういうことかを書いておくことにしました。

先の記事「null 許容参照型と EF Core Code First」で書きましたように、Visual Studio 2022 のテンプレートを使って、ターゲットフレームワークを .NET 6.0 以降の設定にしてプロジェクトを作ると、デフォルトで「Null 許容」オプションが有効になり、Code First の場合は生成されるデータベースのフィールドの NULL 可/不可が、リバースエンジニアリングでデータベースからエンティティクラスを生成する場合はプロパティの型の null 許容/非許容が影響を受けます。

エンティティクラスをビューモデルに使ってブラウザからのデータを MVC アプリのアクションメソッドで受け取る場合、null 許容参照型に注意が必要です。特にナビゲーションプロパティの型による問題は気が付きにくいと思いました。

具体的にどういうことかを下の画像の SQL Server の Blogging データベースの dbo.Blogs, dbo.Posts テーブルを例に使って説明します。

SQL Server データベース

上の dbo.Blogs, dbo.Posts テーブルからリバースエンジニアリングでコンテキストクラスとエンティティクラスを生成します。ターゲットフレームワークは .NET 8.0 で「Null 許容」オプションはプロジェクト全体で有効に設定された状態です。パッケージマネージャコンソールから発行したコマンドを参考に下に載せておきます。

Scaffold-DbContext -Connection "接続文字列" -Provider Microsoft.EntityFrameworkCore.SqlServer -ContextDir Data -OutputDir Models -Tables Posts, Blogs -DataAnnotations

その結果、以下のエンティティクラスが生成されます。(同時にコンテキストクラスも生成されますが省略)

Blog クラス

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace RazorPages1.Models;

public partial class Blog
{
    [Key]
    public int BlogId { get; set; }

    public string Name { get; set; } = null!;

    [InverseProperty("Blog")]
    public virtual ICollection<Post> Posts { get; set; } = 
        new List<Post>();
}

Post クラス

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using Microsoft.EntityFrameworkCore;

namespace RazorPages1.Models;

[Index("BlogId", Name = "IX_Posts_BlogId")]
public partial class Post
{
    [Key]
    public int PostId { get; set; }

    public string Title { get; set; } = null!;

    public string? Content { get; set; }

    public int BlogId { get; set; }
    
    [ForeignKey("BlogId")]
    [InverseProperty("Posts")]
    public virtual Blog Blog { get; set; } = null!;   
}

SQL Server の dbo.Blog テーブルの外部キーフィールド BlogId が NULL 不可になっているため、Blog クラスの Posts ナビゲーションプロパティと、Post クラスの Blog ナビゲーションプロパティの型は Null 許容になりません。

上の Post クラスをベースに、Visual Studio 2022 のスキャフォールディング機能を使って dbo.Posts テーブルの CRUD を行う ASP.NET Core MVC アプリのコントローラーとビューを生成し、アプリを実行してレコードの Create 操作をしようとしたところ ModelState.IsValid が false(検証結果 NG)となって失敗したというのがこの記事の一番上の画像です。Create 操作だけでなく、Edit 操作でも同様に検証 NG で失敗します。

検証 NG となった原因は、Post クラスで null 非許容のナビゲーションプロパティ Bolg に、モデルバインディングの際に null が代入されたからです。

なぜ null が代入されるかですが、下のスキャフォールディングで自動生成された Create 画面を見てください。ブラウザから送信されてくるのは Title, Content, BlogId だけということに注目してください。

Create 画面

一方 Create アクションメソッドの引数は、この記事の一番上の画像の通り Post クラスになっています。ブラウザからデータが送信されてこない PostId, Blog プロパティにはモデルバインディングの際その型のデフォルト値(参照型は null)が代入されます。一番上のデバッグ画面のローカル変数の値を見てください。

Blog ナビゲーションプロパティの型は null 非許容なので、ModelState.IsValid が false になり、Create, Edit に失敗します。

解決策は、生成されたコードに手を加えて Null 許容にすることです。

// 以下のように null 許容にすれば検証は通る
public virtual Blog? Blog { get; set; }

ちなみに、PostId もブラウザからは送信されてきませんのでモデルバインディングでデフォルト値が代入されますが、int 型のデフォルト値は null ではなくて 0 (ゼロ) なので問題は出ません。一番上の画像のローカル変数の青枠を見てください。

また、Title も null 非許容ですが、こちらは検証結果のエラーメッセージがブラウザの画面上の当該テキストボックスの下に表示されるので、すぐ気が付きます。Blog の方はエラーメッセージは出ませんので気づき難いです。

Tags: , , ,

CORE

ASP.NET Core MVC プロジェクトに Identity 実装

by WebSurfer 2024年2月14日 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: , , ,

CORE

ASP.NET Core MVC で Bootstrap5 Modal の利用

by WebSurfer 2023年11月11日 10:46

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

Bootstrap Modal に詳細を表示

この記事ではターゲットフレームワークを .NET 7.0 としています。.NET 5.0 以前をターゲットフレームワークとして作成したプロジェクトでは Bootstrap のバージョンが異なるので注意してください。

この記事のベースとしたアプリは、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

About this blog

2010年5月にこのブログを立ち上げました。主に ASP.NET Web アプリ関係の記事です。

Calendar

<<  2024年3月  >>
252627282912
3456789
10111213141516
17181920212223
24252627282930
31123456

View posts in large calendar