.NET Core MVCとEF Coreのチュートリアルを分かりやすくまとめました①(プロジェクト作成からCRUD機能まで)

スポンサーリンク
プログラミング

ASP.NET Coreを学習中の初心者の頃、どうも公式チュートリアルが分かりづらく困惑していました。そんな私が自分なりの解釈でチュートリアルを分かりやすくまとめたものが以下の記事になります。

https://kintame.site/asp-net-core-mvc-tutorial-index/

当記事では.NET Core MVCとEF Coreのチュートリアルについても分かりやすく解説しようとの試みから作成しました。以下のASP.NET Core MVCとEntity Framework Coreの公式チュートリアルをより分かりやすくまとめたものです。

チュートリアルで分かりにくい表現を言い換えて解説しています。成果物はチュートリアルと同様です。第1回では、プロジェクト作成からCRUD機能までを見ていきます。(公式チュートリアルのPart1~2)

対象となる人

開発環境

  • Windows10またはWindows11
  • Visual Studio 2022
  • .NET 8

事前準備と成果物のイメージ

事前準備として、Visual Studio Installerに「ASP.NETとWeb開発」が追加されていない方はインストールしてください。

 

では、成果物のイメージを確認しましょう。
ここでは架空の大学である「Contoso」の大学向けのWebアプリを作成します。ユーザーは学生、講座、講師の情報を見たり、更新したりできます。

プロジェクト作成

Visual Studio2022の新規プロジェクト作成画面を開き、「ASP.NET Core Web アプリ (Model-View-Controller) 」を選択してください。

プロジェクト名は公式チュートリアルと同様に「ContosoUniversity」とします。

フレームワークは「.NET8.0(長期的なサポート)」を選択し、「作成」をクリックします。

以上で作成は完了です。

サイトタイトルとナビゲーションメニューを変更する

Views/Shared/_Layout.cshtml を開き、以下の変更をします。

  • ContosoUniversity をContoso University に変更します。(3か所)
  • ナビゲーションメニューバーにAbout」、「Students」、「Courses」、「Instructors」、「Departments」を追加します。Privacy」を削除します。

ContosoUniversity→Contoso Universityの変更箇所は以下の通りです。

<head>
 <meta charset="utf-8" />
 <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 <title>@ViewData["Title"] - Contoso University</title><!--変更★-->
 <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
 <link rel="stylesheet" href="~/css/site.css" />
</head>

<!--省略-->

<body>
 <header>
  <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
   <div class="container-fluid">
    <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Contoso University</a><!--変更★-->
    <!--省略-->
    </div>
  </nav>
 </header>
 <!--省略-->
</body>

<!--省略-->

<footer class="border-top footer text-muted">
<div class="container">
&copy; 2024 - Contoso University - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a><!--変更★-->
</div>
</footer>

 

ナビゲーションメニューバーにAbout」、「Students」、「Courses」、「Instructors」、「Departments」を追加し、Privacy」を削除する変更は以下の通りです。

<body>
 <header>
  <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
   <div class="container">
    <!--省略-->
    <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
     <ul class="navbar-nav flex-grow-1">
      <li class="nav-item">
       <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
      </li>
      <!--変更開始★-->
      <li class="nav-item">
       <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="About">About</a>
      </li>
      <li class="nav-item">
       <a class="nav-link text-dark" asp-area="" asp-controller="Students" asp-action="Index">Students</a>
      </li>
      <li class="nav-item">
       <a class="nav-link text-dark" asp-area="" asp-controller="Courses" asp-action="Index">Courses</a>
      </li>
      <li class="nav-item">
       <a class="nav-link text-dark" asp-area="" asp-controller="Instructors" asp-action="Index">Instructors</a>
      </li>
      <li class="nav-item">
       <a class="nav-link text-dark" asp-area="" asp-controller="Departments" asp-action="Index">Departments</a>
      </li>
      <!--変更終了★-->
     </ul>
    </div>
   </div>
  </nav>
 </header>
</body>

Home/Index.cshtmlを変更する

Views/Home/Index.cshtml を、次の内容に上書きします。

/* Your code... */
@{
 ViewData["Title"] = "Home Page";
}
<div class="jumbotron">
 <h1>Contoso University</h1>
</div>
<div class="row">
 <div class="col-md-4">
  <h2>Welcome to Contoso University</h2>
  <p>
    Contoso University is a sample application that demonstrates how to use Entity Framework Core in an ASP.NET Core MVC web application. 
  </p>
 </div>
 <div class="col-md-4">
  <h2>Build it from scratch</h2>
  <p>You can build the application by following the steps in a series of tutorials.</p>
  <p><a class="btn btn-default" href="https://docs.asp.net/en/latest/data/ef-mvc/intro.html">See the tutorial &raquo;</a></p>
 </div>
 <div class="col-md-4">
  <h2>Download it</h2>
  <p>You can download the completed project from GitHub.</p>
  <p><a class="btn btn-default" href="https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-mvc/intro/samples/5cu-final">See project source code &raquo;</a></p>
 </div>
</div>

アプリを実行してみましょう。以下のように表示されていれば問題ありません。

以上でベースのプロジェクト作成まで完了しました。ここからEF Coreを使用したWebアプリケーションを作成していきます。

EF Core NuGet Packageの追加

ASP.NET CoreのプロジェクトでEntity Framework Core(以降EF Core)を使用するには、プロジェクトにNuGetパッケージを追加する必要があります。このチュートリアルではSQLServerを使用します。

Entity Framework Core(EFCore)とは
オブジェクトリレーショナルマッパー(ORM)で、SQLを記述せずにデータベースをコーディングで行える技術です。クロスプラットフォームでSQL Server、SQLite、MySQL、PostgreSQLなどに対応しています。

パッケージマネージャーコンソールで以下を実行します。

Install-Package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.SqlServer

これでNuGet Packageが追加されました。

データベースの作成

ここからはアプリのデータを保持するデータベースの作成を行っていきます。

その前に、MVC(Model-View-Controller)アーキテクチャにおけるモデルとコントローラー、ビューの役割を復習しておきましょう。

Model…データを担当する部分。
View…画面の表示を担当する部分。
Controller…ユーザーからの入力を受け取り、処理を行なう部分。
それではデータベースの作成を進めていきます。

エンティティクラスの作成

ここでは3つのエンティティクラスを作成します。

エンティティはデータベース内の特定のデータを表すもので、プロパティとリレーションを持ちます。MVCモデルにおいて、エンティティはモデルのインスタンス(実体)と考えるといいと思います。

チュートリアルの例で言うと、
講座(Course)エンティティ:講座の情報を表します。講座ID、講座タイトル、単位数のプロパティを持ちます。登録エンティティとのリレーションを持ちます。
登録(Enrollment)エンティティ:生徒の講座の登録情報を表します。登録ID、講座ID、生徒ID、成績のプロパティを持ちます。講座エンティティと生徒エンティティの両方とリレーションを持ちます。
生徒(Student)エンティティ:生徒の情報を表します。生徒ID、苗字、名前、登録日時のプロパティを持ちます。登録エンティティとのリレーションを持ちます。

では、ModelsフォルダーにStudentエンティティクラスを作成しましょう。

using System;
using System.Collections.Generic;

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

  public ICollection<Enrollment> Enrollments { get; set; } = new List<Enrollment>();
 }
}

抑えておくべきポイントは2点あります。

1つ目はIDプロパティです。EF Coreでは「ID」または「クラス名ID」と名付けられたプロパティを主キーとしてテーブルを作成します。この例だと「ID」の代わりに「StudentID」と命名することで主キーとしてテーブル作成されます。

2つ目はナビゲーションプロパティです。これはこのエンティティに紐づく他のエンティティを含められます。この例だとEnrollmentsプロパティがナビゲーションプロパティです。具体的には、生徒がどの講座でどの成績だったか、データを扱いたい際に生徒エンティティから登録エンティティの情報を参照出来るようになります。
ナビゲーションプロパティが複数のエンティティに紐づく可能性が場合はIList<T>やICollection<T>、List<T>である必要があります。

続けて、ModelsフォルダーにEnrollmentエンティティクラスを作成していきましょう。

namespace ContosoUniversity.Models {
 public enum Grade { A, B, C, D, F }

 public class Enrollment { 
   public int EnrollmentID { get; set; } 
   public int CourseID { get; set; } 
   public int StudentID { get; set; } 
   public Grade? Grade { get; set; } 
   public Course Course { get; set; } 
   public Student Student { get; set; } 
 }
}

ここでは特徴的なプロパティを確認しておきます。
1つ目はGradeプロパティです。enum型(列挙型)でGrade?となっていますが、Grade?はnull許容で成績が0ではなく未設定な項目を用意するためのものです。
2つ目はStudentIDプロパティです。これは外部キー(FK)でStudentというナビゲーションプロパティに紐づきます。先に説明したStudentエンティティクラスのEnrollmentsナビゲーションプロパティと違い、コレクション要素ではないため、1つのエンティティしか紐づきません。CourseIDプロパティも紐づくのがCourseエンティティであること以外は同じです。

ここで押さえておくべきポイントは外部キー(FK)プロパティの命名規則です。
主キーと同じように外部キープロパティには命名規則があり、「<ナビゲーションプロパティ名><主キープロパティ名>」または「<ナビゲーションプロパティのエンティティの主キーのプロパティ名>」の形式になっている場合は外部キーとして扱われます。具体例を見ると、Studentエンティティの主キーはIDなので、StudentIDとなります。一方でCourseエンティティの主キーはCourseIDですが、CourseIDとしても外部キープロパティとして扱われます。これらは好みやプロジェクトの規則に合わせて使い分けて大丈夫です。

では、残りのCourseエンティティクラスもModelsフォルダに追加しましょう。

using System.Collections.Generic; 
using System.ComponentModel.DataAnnotations.Schema; 
namespace ContosoUniversity.Models { 
 public class Course { 
  [DatabaseGenerated(DatabaseGeneratedOption.None)] 
  public int CourseID { get; set; } 
  public string Title { get; set; } 
  public int Credits { get; set; } 
  public ICollection<Enrollment> Enrollments { get; set; } = new List<Enrollment>();
 } 
}

[DatabaseGenerated(DatabaseGeneratedOption.None)]はCourseIDに設定されています。DatabaseGenerated属性はデータベースがプロパティの値を生成する方法を指定する属性で、ここでは(DatabaseGeneratedOption.None)によって主キーを自動生成しない設定を指定しています。

以上で、エンティティクラスの作成は完了です。

データベースコンテキストにテーブル作成とテーブル名称変更を追加する

データベースコンテキストはデータモデルのエンティティクラスを管理します。データベースで作成するテーブルの情報やリレーションシップ構成などを定義できます。テーブル追加時にはほぼ必ず触ることになるので、覚えておくと良いでしょう。Microsoft.EntityFrameworkCore.DbContext クラスから派生させて作成します。

ここではSchoolContextクラスを作成します。プロジェクトフォルダにDataフォルダを作成し、Dataフォルダに新規>クラス でクラスを追加しましょう。

using ContosoUniversity.Models; 
using Microsoft.EntityFrameworkCore; 
namespace ContosoUniversity.Data { 
 public class SchoolContext : DbContext { 
  public SchoolContext(DbContextOptions<SchoolContext> options) : base(options) { } 
  public DbSet<Course> Courses { get; set; } 
  public DbSet<Enrollment> Enrollments { get; set; } 
  public DbSet<Student> Students { get; set; } 
 } 
}

ここで抑えておきたいのはDbSetプロパティです。
public DbSet<Course> Courses { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
の部分で、これらはデータベースのテーブルと対応しています。具体的には、Studentsプロパティは Students テーブルに対応します。Studentsというように複数形になっているのはStudentのコレクションを持つ要素だからです。

public DbSet<Student> Students { get; set; }を記載した場合、DbSet<Enrollment>  DbSet<Course> は省略してもテーブルは自動生成されます。その理由は、Student エンティティが Enrollment エンティティを参照し、Enrollment エンティティが Course エンティティを参照しているためです。

このままデータベースを作成するとテーブル名はそれぞれcourses、enrollments、studentsと複数系での名前がつけられてしまいます。これを変更するためにはOnModelCreatingメソッドを上書きしてあげる必要があります。
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");

using ContosoUniversity.Models; 
using Microsoft.EntityFrameworkCore; 
namespace ContosoUniversity.Data { 
 public class SchoolContext : DbContext { 
  public SchoolContext(DbContextOptions<SchoolContext> options) : base(options) { } 
  public DbSet<Course> Courses { get; set; } 
  public DbSet<Enrollment> Enrollments { get; set; } 
  public DbSet<Student> Students { get; set; } 

  //以下を追記
  protected override void OnModelCreating(ModelBuilder modelBuilder) { 
   modelBuilder.Entity<Course>().ToTable("Course"); 
   modelBuilder.Entity<Enrollment>().ToTable("Enrollment"); 
   modelBuilder.Entity<Student>().ToTable("Student"); 
  }
 } 
}

これでテーブル名の変更は完了です。

データベースコンテキストの登録

ASP.NET Coreでは、DI(依存性注入)コンテナが組み込まれており、サービスを登録して利用する必要があります。ここでは依存性注入の詳細まで解説できませんが、再利用性やテストをしやすくするためのものと思っておいてください。

SchoolContextというデータベースコンテキストをサービスとして登録し、コントローラーで利用する方法を説明します。Program.csファイルを開き、ConfigureServicesメソッドに以下のコードを追加します。

//以下2行を追加
using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<SchoolContext>(options => 
     options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

var app = builder.Build();

// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
  app.UseExceptionHandler("/Home/Error");
  // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 
  app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapControllerRoute( 
   name: "default",
   pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

これでSchoolContextが読み込まれ、接続にSQLServerが用いられ、接続文字列にDefaultConnectionが用いられるようになりました。では、接続文字列の設定も追加してあげます。appsettings.json を開きます。

{ 
  "ConnectionStrings": { 
   "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity1;Trusted_Connection=True;MultipleActiveResultSets=true" 
  },
  "Logging": { 
   "LogLevel": { 
    "Default": "Information", 
    "Microsoft": "Warning", 
    "Microsoft.Hosting.Lifetime": "Information"
   }
  },
 "AllowedHosts": "*" 
}

これで接続設定は完了です。

開発環境で詳細なエラー情報が必要な場合は、以下のデータベース例外フィルターも設定しておくことをオススメしますProgram.csファイルを開き、ServicesAddDatabaseDeveloperPageExceptionFilterを追加します。

builder.Services.AddControllersWithViews();
builder.Services.AddDbContext<SchoolContext>(options => 
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddDatabaseDeveloperPageExceptionFilter();//左記を追記

これまでの設定において、SQLServer LocalDBに接続する準備が整いました。デフォルトではユーザーディレクトリ(C:¥Users¥user)にデータベースファイル.mdbが生成されます。

テストデータでDBを初期化する

EF により、空のデータベースが作成されます。ここでは、データベースの自動生成後にテスト データを設定するために呼び出されるメソッドを追加します。EnsureCreated メソッドを使用して、データベースを自動的に作成します。Data フォルダーに、DbInitializerという名前の新しいクラスを作成します。

using ContosoUniversity.Models; 
using System; 
using System.Linq; 

namespace ContosoUniversity.Data { 
 public static class DbInitializer { 
  public static void Initialize(SchoolContext context) { 
   context.Database.EnsureCreated();

   //生徒テーブルの存在確認
   if (context.Students.Any()) { 
    return; // 生成済みなら何もしない
   } 

   var students = new Student[] { 
    new Student{FirstMidName="Carson",LastName="Alexander",EnrollmentDate=DateTime.Parse("2005-09-01")}, 
    new Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Parse("2002-09-01")}, 
    new Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse("2003-09-01")}, 
    new Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Parse("2002-09-01")}, 
    new Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2002-09-01")}, 
    new Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Parse("2001-09-01")}, 
    new Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse("2003-09-01")}, 
    new Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Parse("2005-09-01")} }; 
    foreach (Student s in students) { 
     context.Students.Add(s); 
    }
    context.SaveChanges();

    var courses = new Course[] {
     new Course{CourseID=1050,Title="Chemistry",Credits=3}, 
     new Course{CourseID=4022,Title="Microeconomics",Credits=3}, 
     new Course{CourseID=4041,Title="Macroeconomics",Credits=3}, 
     new Course{CourseID=1045,Title="Calculus",Credits=4}, 
     new Course{CourseID=3141,Title="Trigonometry",Credits=4}, 
     new Course{CourseID=2021,Title="Composition",Credits=3}, 
     new Course{CourseID=2042,Title="Literature",Credits=4} 
    }; 
    foreach (Course c in courses) { 
     context.Courses.Add(c); 
    } 
    context.SaveChanges(); 

    var enrollments = new Enrollment[] { 
     new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A}, 
     new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C}, 
     new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B}, 
     new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B}, 
     new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F}, 
     new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F}, 
     new Enrollment{StudentID=3,CourseID=1050}, 
     new Enrollment{StudentID=4,CourseID=1050}, 
     new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F}, 
     new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C}, 
     new Enrollment{StudentID=6,CourseID=1045}, 
     new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A}, 
    }; 
    foreach (Enrollment e in enrollments) { 
     context.Enrollments.Add(e); 
    } 
    context.SaveChanges(); 
   } 
  } 
}

 Program.cs で上の初期化処理が読み込まれるように設定を追加します。

//省略
var app = builder.Build();

//以下の行を追記
CreateDbIfNotExists(app);

//省略

app.Run();

//以下の行を追記
void CreateDbIfNotExists(IHost host)
{
   using var scope = host.Services.CreateScope();
   var services = scope.ServiceProvider;
   try
   {
      var context = services.GetRequiredService<SchoolContext>();
      DbInitializer.Initialize(context);
   }
   catch (Exception ex)
   {
      var context = services.GetRequiredService<SchoolContext>();
      DbInitializer.Initialize(context);
   }
   catch (Exception ex)
   {
      var logger = services.GetRequiredService<ILogger<Program>>();
      logger.LogError(ex, "An error occurred creating the DB.");
   }
}

これでアプリの実行時にデータベースが作成されて、テスト用のデータが読み込まれます。表示→SQLServerオブジェクトエクスプローラーを開き、データベースとテーブルが作成されていることが確認できます。

Studentコントローラーとビューの作成

ではデータモデルに基づいてコントローラーとビューを自動生成する、スキャフォールディングを実行していきましょう。繰り返しになりますが、MVCモデルのそれぞれの役割をおさらいしておきましょう。

Model…データを担当する部分。
View…画面の表示を担当する部分。
Controller…ユーザーからの入力を受け取り、処理を行なう部分。
Controllersフォルダで右クリックし、追加>新規スキャフォールディングアイテムを選択します。[Entity Framework を使用したビューがある MVC コントローラー] を選択します。ModelクラスにStudentを選択し、Data context クラスはSchoolContextを選択します。

 

Student のスキャフォールディング
AddをするとControllersフォルダ内に StudentsController.cs が作成され、ViewsフォルダにStudentsフォルダとIndex.cshtml、Create.cshtml、Edit.cshtml、Details.cshtml、Delete.cshtmlが作成されます。

アプリを実行する

アプリを実行し、ナビゲーションリンクのStudentsをクリックするとデータが表示されます。アプリの初回実行時にデータベースがなければテストデータを作成する処理を記述したので、あらかじめデータが用意されます。

データベースの詳細は表示>SQLServerオブジェクトエクスプローラーから(localdb)\MSSQLLocalDB >ContosoUniversity1を選択し、テーブル>dbo.Studentを右クリックしてデータの表示を押すことで確認できます。

ちなみに、EFCoreでは非同期でデータベースへのアクセスを行います。実際にはクエリを作成した段階では同期処理で、データベース操作を実行する時にのみ非同期で実行されます。

以下のように実行後、初期データが表示されていれば、正しく進められています。

StudentのCRUD機能のカスタマイズ

EntityFrameworkによって自動生成されたStudentのビューではコレクション要素のナビゲーションプロパティのEnrollmentsが省略されています。そのため、どの生徒がどの講座を登録したかわからない状態です。ここからはデータの紐づきが見えるようにCRUDの各ビューでスキャフォールディングで自動生成したコントローラーとビューをカスタマイズしていきます。

ちなみに、CRUDは(Create、Read、Update、Delete)の略です。

Controllers/StudentsController.csのDetailsアクションに以下の処理を追記します。

public async Task<IActionResult> Details(int? id) {
 if (id == null) { return NotFound(); }

 //以下を追記
 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 View(student); 
}

ここで抑えておきたいのはIncludeThenIncludeです。Studentのデータを取得する際に関連するナビゲーションプロパティであるStudent.EnrollmentsIncludeによって読み込まれます。ThenIncludeではEnrollmentsのナビゲーションプロパティであるEnrollment.Courseが読み込まれます。
こうすることで、生徒が登録している講座情報にアクセスできるようになります。

AsNoTrackingは読み取り専用データの場合に設定することでパフォーマンス向上が見込めます。

ルートデータ

Detailsメソッドはidを引数として取れますが、これはProgram.csのapp.UseEndPointでルートデータが設定されています。例えば、デフォルトではController/Action/idが設定されています。

app.UseEndpoints(endpoints =>
{
  endpoints.MapControllerRoute(
     name: "default",
     pattern: "{controller=Home}/{action=Index}/{id?}"); 
});

ControllerとActionにはデフォルト値が設定されているため、以下のようなルートにアクセスするとHomeControllerのIndexアクションが呼びだされます。

http://localhost:5000/

またIndex.cshtmlでは、Razor ビューのタグ ヘルパーによって、ハイパーリンクの URL が作成されます。 次の 記述はIndex.cshtmlに書かれたRazor文です。

<a asp-action="Edit" asp-route-id="@item.ID">Edit</a>

asp-route-〇〇の部分が id になっておりid パラメーターが既定のルートと一致するため、id がルート データに追加されます。これにより、item.ID が 6 のときは次の HTML が生成されます。

<a href="/Students/Edit/6">Edit</a>

一方で、以下のRazor文では、studentID は既定ルートのパラメーターと一致しないため、クエリ文字列として追加されます。

<a asp-action="Edit" asp-route-studentID="@item.ID">Edit</a>

item.ID が 6 のときは次の HTML が生成されます。

<a href="/Students/Edit?studentID=6">Edit</a>

タグ ヘルパーについては、「ASP.NET Core のタグ ヘルパー」をご覧ください。

Students/Details.cshtmlにEnrollmentを追加する

Views/Students/Details.cshtml を開きます。 以下の例のように、DisplayNameFor および DisplayFor ヘルパーを使って各プロパティの値を画面に出力できます。

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

最後のフィールドの後、終了タグ </dl> の直前に、登録の一覧を表示する次のコードを追加します。

<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>

インデントが乱れた場合は、Ctrl + D + K キーで調整できます。

Enrollments ナビゲーション プロパティ内のエンティティをループ処理します。 登録ごとに、コースタイトルと学年が表示されます。 コース タイトルは、Enrollments エンティティの Course ナビゲーション プロパティに格納されている Course エンティティから取得されます。

先のデータ取得時にIncludeでEnrollmentsナビゲーションプロパティを紐付け、ThenIncludeでCourseナビゲーションプロパティを紐付けたため、データにアクセスできるようになります。

  .Include(s => s.Enrollments)
    .ThenInclude(e => e.Course)

では、アプリを実行し、 [Students] タブを選んで、受講者の [Details] リンクをクリックします。 選んだ受講者のコースとグレードの一覧が表示されます。

 

Students/Create.cshtmlを更新する

続けて作成ページの更新を進めましょう。StudentsController.cs で、try-catch ブロックを追加し、Bind 属性から ID を削除して、HttpPost の Create メソッドを変更します。

[HttpPost] 
[ValidateAntiForgeryToken] 
public async Task<IActionResult> Create( 
//以下からIDを削除し、tryで囲む
 [Bind("EnrollmentDate,FirstMidName,LastName")] Student student) {
  try {
   if (ModelState.IsValid) { 
    _context.Add(student);
    await _context.SaveChangesAsync();
    return RedirectToAction(nameof(Index)); 
   }
  } 
 //catchも追記
 catch (DbUpdateException /* ex */) { 
  //Log the error (uncomment ex variable name and write a log.
  ModelState.AddModelError("", "Unable to save changes. "
  + "Try again, and if the problem persists " 
  + "see your system administrator.");
 }
 return View(student); 
}

ここでのポイントはモデルバインダーと呼ばれるBind属性の役割です。ここではフォームの値を自動的にStudentインスタンスに当てはめてくれます。

モデル バインダー
フォームによって送信されたデータの操作を容易にする ASP.NET Core MVC の機能です。ポストされたフォームの値をパラメーター内のアクション メソッドに渡します。
_context.Add(student);

上記ではモデル バインダーによって作成された Student エンティティをStudentsエンティティセットに追加します。その後、await _context.SaveChangesAsync();変更をデータベースに保存します。

ID は行が挿入されるときに SQL Server によって自動的に設定される主キー値であるため、Bind 属性から削除しました。 こうすることでユーザーからの入力によって ID 値が設定されることはありません。

 

ValidateAntiForgeryToken 属性は、クロスサイト リクエスト フォージェリ (CSRF) 攻撃の対策として付与しておくことをオススメします。

Students/Create.cshtmlをテストする

アプリを実行し、 [Students] タブを選んで、 [Create New] をクリックします。

名前と日付を入力します。 この時、無効な日付を入力してみてください(例えば、2024年9月31日)。その後、 [Create] をクリックしてエラー メッセージを確認します。

Views/Students/Create.cshtml 内のコードでは、各フィールドに対して labelinput、および span (検証メッセージ用) の各タグ ヘルパーが使われています。既定で作成されるサーバー側の検証です。後ほどクライアント側で検証するための属性についても触れていきます。

タグヘルパー
@、@{  }で記述するASP.NET CoreでHTML要素にサーバーサイドのロジックを組み込むための機能です。これにより、HTMLとC#コードの統合が容易になり、開発効率が向上します。また、IntelliSenseのサポートにより、コード補完が可能で、再利用性とメンテナンス性も向上します。

Students/Edit.cshtmlを更新する

StudentController.cs に含まれるHttpPost の Edit アクション メソッドを、次のコードに置き換えます。(注意|※HttpGetのEditアクションではありません)

[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken] 
public async Task<IActionResult> EditPost(int? id) {
 if (id == null) { return NotFound(); }
 var studentToUpdate = await _context.Students.FirstOrDefaultAsync(s => s.ID == id);
 if (await TryUpdateModelAsync<Student>( 
   studentToUpdate,
   "",
   s => s.FirstMidName, s => s.LastName, s => s.EnrollmentDate))
 { 
   try { 
    await _context.SaveChangesAsync(); return RedirectToAction(nameof(Index));
   }
   catch (DbUpdateException /* ex */) { 
    //Log the error (uncomment ex variable name and write a log.)
    ModelState.AddModelError("", "Unable to save changes. " +
      "Try again, and if the problem persists, " + 
      "see your system administrator."); 
   }
 }
 return View(studentToUpdate); 
}

この実装方法は過剰ポスティングを防ぐためのセキュリティを意識した最善の方法です。 既存のエンティティを読み取り、TryUpdateModel を呼び出して、更新可能にするフィールドを、TryUpdateModel パラメーターで宣言します。この例だと、FirstMidName、LastName、EnrollmentDateのみを検証して更新します。

Students/Edit.cshtmlをテストする

アプリを実行し、 [Students] タブを選んで、 [Edit] ハイパーリンクをクリックします。

 

データを編集し、 [Save] をクリックします。 [Index] ページが開き、編集したデータが表示されていればOKです。

Students/Delete.cshtmlを更新する

StudentController.csで実装されているHttpGet の Delete メソッドは、Details および Edit メソッドと同様にFirstOrDefaultAsync メソッドを使って、選択されたStudent エンティティを取得します。 ここでは、もしSaveChanges の呼び出しが失敗したときに、カスタムエラーメッセージを実装するための方法を追加で実装します。

最初にtry-catch ブロックを HttpPost の Delete メソッドに追加します。HttpGet の Delete アクション メソッドを、次のコードに置き換えます。

// GET: Students/Delete/5
public async Task<IActionResult> Delete(int? id, bool? saveChangesError = false){
 if (id == null) { return NotFound(); }
 var student = await _context.Students
  .AsNoTracking()//追記
  .FirstOrDefaultAsync(m => m.ID == id);
 if (student == null) { return NotFound(); }

 //以下を追記
 if (saveChangesError.GetValueOrDefault()) {
   ViewData["ErrorMessage"] = 
   "Delete failed. Try again, and if the problem persists " +
   "see your system administrator."; 
 }
 //↑ここまで
 return View(student); 
}

一方でHttpPostのDeleteアクションメソッドも更新していきます。

// POST: Students/Delete/5
[HttpPost, ActionName("Delete")] 
[ValidateAntiForgeryToken] 
public async Task<IActionResult> DeleteConfirmed(int id) 
{
  var student = await _context.Students.FindAsync(id);
  //以下に更新
  if (student == null) {
   return RedirectToAction(nameof(Index)); 
  }

  //以下に更新
  try
  {
   _context.Students.Remove(student);
   await _context.SaveChangesAsync();
   return RedirectToAction(nameof(Index));
  }
  //以下に更新
  catch (DbUpdateException /* ex */)
  {
   //Log the error (uncomment ex variable name and write a log.)
   return RedirectToAction(nameof(Delete), new { id = id, saveChangesError = true });
  } 
}

このコードは、選択されたエンティティを取得した後、Remove メソッドを呼び出して、エンティティの状態を Deleted に設定します。 SaveChanges が呼び出されると、SQL DELETE コマンドが生成されます。

Students/Delete.cshtmlを更新してテストする

Views/Student/Delete.cshtml で、次の例に示すように、h1 見出しと h3 見出しの間にエラー メッセージを追加します。

<h1>Delete</h1> 
<p class="text-danger">@ViewData["ErrorMessage"]</p> <!-- 追記 -->
<h3>Are you sure you want to delete this?</h3>

では、アプリを実行し、 [Students] タブを選んで、 [Delete] ハイパーリンクをクリックします。

[Delete] をクリックします。 削除された学生を含まない [Index] ページが表示されます。

補足:データベース接続リソースの解放について

データベース接続が保持しているリソースはASP.NET Core に組み込まれている依存関係の挿入が、自動的に行ってくれます。開発時はインスタンスの解放を意識することなく開発することができます。

補足:トランザクション処理について

EntityFrameworkのSaveChangeメソッドではトランザクション処理を意識する必要はありません。SaveChangesメソッドの実行は完全に成功するか、全ての変更が失敗するかのどちらかになります。例えば、3つのINSERT処理があり、うち1つでも失敗すれば3つともの変更はロールバックされて全て失敗となります。

トランザクションとは
複数の処理を一つの処理として一貫性を持たせて実行・管理する仕組みです。トランザクション処理の例としてわかりやすいのが「銀行口座からの引き出し」です。 これは「預金残高から一定の金額を引き出す」という処理と「引き出した金額分を預金残高から減らす」という処理が連動して行われています。

 

このチュートリアルはここまでです!次は以下のチュートリアルに進みましょう!

.NET Core MVCとEF Coreのチュートリアルを分かりやすくまとめました②(並び替え/フィルター/ページングからマイグレーションまで)
.NET Core MVCとEF Coreのチュートリアルについても分かりやすく解説するブログの第2回です。前回の記事はこちらです。 当記事では以下のASP.NET Core MVCとEntity Framework ...

 

 

 

コメント

タイトルとURLをコピーしました