チュートリアル: ASP.NET MVC Web アプリでの EF Core の概要

作成者: Tom DykstraRick Anderson

このチュートリアルでは、ASP.NET Core MVC と Entity Framework Core のコントローラーとビューについて説明します。 Razor Pages は代替プログラミング モデルです。 新しい開発では、コントローラーやビューを使う MVC よりも Razor Pages を使うことをお勧めします。 このチュートリアルの Razor Pages バージョンを参照してください。 それぞれのチュートリアルには、もう一方では説明されない内容が含まれています。

この MVC のチュートリアルに含まれ、Razor Pages のチュートリアルには含まれていないこと:

  • データ モデルで継承を実装する
  • 生 SQL クエリを実行する
  • 動的な LINQ を使ってコードを簡略化する

Razor Pages のチュートリアルに含まれ、このチュートリアルには含まれていないこと:

  • Select メソッドを使って関連データを読み込む
  • EF のベスト プラクティス。

このチュートリアルは、ASP.NET Core 6 用に更新されていません。 ASP.NET Core 6 の Web テンプレートでは、Startup.csProgram.cs が 1 つの Program.cs ファイルに統合された新しい最小ホスティング モデルが使用されています。 このチュートリアルが更新されるまで、新しい最小ホスティング モデルで EF を使用する方法については、「ASP.NET Core での Entity Framework Core を使用した Razor Pages - チュートリアル 1/8」と「パート 4、ASP.NET Core MVC アプリにモデルを追加する」をご覧ください。 ASP.NET Core 6 向けのチュートリアルの更新については、この GitHub イシューで追跡しています。

Contoso University のサンプル Web アプリでは、Entity Framework (EF) Core と Visual Studio を使用して ASP.NET Core MVC Web アプリを作成する方法を示します。

このサンプル アプリは架空の Contoso University の Web サイトです。 学生の受け付け、講座の作成、講師の割り当てなどの機能が含まれています。 これは、Contoso University のサンプル アプリを作成する方法を説明するチュートリアル シリーズの 1 回目です。

前提条件

  • ASP.NET Core MVC を初めて使用する場合は、これを開始する前に、「ASP.NET Core MVC の概要」チュートリアル シリーズをご覧ください。

データベース エンジン

Visual Studio の手順では、SQL Server LocalDB を使用します。これは、Windows 上でのみ実行される SQL Server Express のバージョンです。

問題の解決とトラブルシューティング

解決できない問題に遭遇した場合、通常、完成済みのプロジェクトと自分のコードを比較することで解決策がわかります。 一般的なエラーとその解決方法の一覧については、このシリーズの最後のチュートリアルにあるトラブルシューティングのセクションをご覧ください。 そこで必要な答えが見つからない場合、StackOverflow.com で ASP.NET Core または EF Core に関する質問を投稿できます。

ヒント

これは 10 回のチュートリアルからなるシリーズであり、いずれの回も前のチュートリアルを基盤にしています。 チュートリアルが完了したら、毎回、プロジェクトのコピーを保存するようお勧めします。 問題に遭遇したとき、前のチュートリアルから始めることができます。シリーズ全体の始めまで戻る必要がありません。

Contoso University Web アプリ

このチュートリアル シリーズで作成するアプリは、大学向けの基本的な Web サイトです。

ユーザーは学生、講座、講師の情報を見たり、更新したりできます。 アプリの画面のいくつかを次に示します。

Students Index page

Students Edit page

Web アプリを作成する

  1. Visual Studio を開始し、 [新しいプロジェクトの作成] を選択します。
  2. [新しいプロジェクトの作成] ダイアログで、[ASP.NET Core Web アプリケーション]>[次へ] の順に選択します。
  3. [新しいプロジェクトの構成] ダイアログで、 [プロジェクト名] に「ContosoUniversity」と入力します。 コードをコピーするときに各namespaceが一致するように、この正確な名前 (大文字と小文字を含む) を使用することが重要です。
  4. [作成] を選択します
  5. [新しい ASP.NET Core Web アプリケーションの作成] ダイアログで、次のものを選択します。
    1. ドロップダウンで [.NET Core][ASP.NET Core 5.0]
    2. ASP.NET Core Web アプリ (Model-View-Controller)
    3. [作成]New ASP.NET Core Project dialog

サイトのスタイルを設定する

基本的な変更をいくつか行い、サイトのメニュー、レイアウト、ホーム ページを決めます。

Views/Shared/_Layout.cshtml を開き、次の変更を行います。

  • ContosoUniversityContoso University に変更します。 これは 3 回出てきます。
  • メニュー エントリとして「About」、「Students」、「Courses」、「Instructors」、「Departments」を追加し、「Privacy」メニュー エントリを削除します。

次のコードでは上の変更が強調表示されています。

<!DOCTYPE html>
<html lang="en">
<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">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Contoso University</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <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>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2020 - Contoso University - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

Views/Home/Index.cshtml で、ファイルの内容を次のマークアップに置き換えます。

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

CTRL を押しながら F5 を押してプロジェクトを実行するか、メニューで [デバッグ] > [デバッグなしで開始] の順に選択します。 ホーム ページに、このチュートリアルで作成されるページのタブが表示されます。

Contoso University home page

EF Core の NuGet パッケージ

このチュートリアルでは SQL Server を使用します。プロバイダー パッケージは Microsoft.EntityFrameworkCore.SqlServer です。

EF SQL Server パッケージとその依存関係 Microsoft.EntityFrameworkCoreMicrosoft.EntityFrameworkCore.Relational により、EF のランタイム サポートが提供されます。

Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore NuGet パッケージを追加します。 パッケージ マネージャー コンソール (PMC) で、次のコマンドを入力して NuGet パッケージを追加します。

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

Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore NuGet パッケージには EF Core のエラー ページ用の ASP.NET Core ミドルウェアが用意されています。 このミドルウェアは、EF Core の移行に関するエラーを検出して診断するのに役立ちます。

EF Core で利用できるその他のデータベース プロバイダーに関しては、「データベース プロバイダー」を参照してください。

データ モデルを作成する

このアプリ用に次のエンティティ クラスを作成します。

Course-Enrollment-Student data model diagram

上のエンティティには、次のリレーションシップがあります。

  • Student エンティティと Enrollment エンティティの間の一対多リレーションシップ。 1 人の学生を任意の数の講座に登録できます。
  • Course エンティティと Enrollment エンティティの間の一対多リレーションシップ。 1 つの講座に任意の数の学生を登録できます。

次のセクションでは、これらの各エンティティに対してクラスを作成します。

Student エンティティ

Student entity diagram

以下のコードを使用して、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; }
    }
}

ID プロパティは、このクラスに相当するデータベース テーブルの主キー (PK) 列です。 既定では、EF は、ID または classnameID という名前のプロパティを主キーとして解釈します。 たとえば、PK には ID ではなく StudentID という名前を付けることができます。

Enrollments プロパティはナビゲーション プロパティです。 ナビゲーション プロパティには、このエンティティに関連する他のエンティティが含まれます。 Student エンティティの Enrollments プロパティ:

  • その Student エンティティに関連するすべての Enrollment エンティティが含まれます。
  • データベースの特定の Student 行に、関連する Enrollment 行が 2 つある場合:
    • その Student エンティティの Enrollments ナビゲーション プロパティには、これら 2 つの Enrollment エンティティが含まれます。

Enrollment 行には、StudentID 外部キー (FK) 列の学生の PK 値が含まれます。

ナビゲーション プロパティが複数のエンティティを保持できる場合:

  • 型は、ICollection<T>List<T>HashSet<T> などのリストである必要があります。
  • エンティティの追加、削除、更新を行うことができます。

多対多および一対多のナビゲーション リレーションシップは、複数のエンティティを含むことができます。 ICollection<T> を使用する場合、EF によって HashSet<T> コレクションが既定で作成されます。

Enrollment エンティティ

Enrollment entity diagram

以下のコードを使用して、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; }
    }
}

EnrollmentID プロパティは PK です。 このエンティティには、ID 自体ではなく classnameID パターンが使用されます。 Student エンティティには ID パターンが使用されていました。 データ モデル全体で 1 つのパターンを使用することを好む開発者もいます。 このチュートリアルのバリエーションでは、どちらのパターンも使用できることが示されています。 後のチュートリアルでは、クラス名なしの ID を使用して、データ モデルに継承を簡単に実装する方法を示します。

Grade プロパティは enum です。 Grade の型宣言の後にある ? は、Grade プロパティが Null 許容であることを示します。 null という成績は、成績が 0 であるということではありません。 null は、成績がわからないこと、またはまだ割り当てられていないことを意味します。

StudentID プロパティは外部キー (FK) であり、それに対応するナビゲーション プロパティは Student です。 Enrollment エンティティは 1 つの Student エンティティに関連付けられるので、プロパティで保持できる Student エンティティは 1 つだけです。 これは、複数の Enrollment エンティティを保持できる Student.Enrollments ナビゲーション プロパティとは異なります。

CourseID プロパティは FK で、それに対応するナビゲーション プロパティは Course です。 Enrollment エンティティは 1 つの Course エンティティに関連付けられます。

プロパティの名前が <ナビゲーション プロパティ名><主キー プロパティ名> になっている場合、それは Entity Framework により FK プロパティとして解釈されます。 たとえば、Student ナビゲーション プロパティに対する StudentID (Student エンティティの PK は ID であるため)。 FK プロパティは、<主キー プロパティ名> という名前にすることもできます。 たとえば、CourseID (Course エンティティの PK が CourseID であるため)。

Course エンティティ

Course entity diagram

以下のコードを使用して、Models フォルダーに Course クラスを作成します。

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

Enrollments プロパティはナビゲーション プロパティです。 1 つの Course エンティティにたくさんの Enrollment エンティティを関連付けることができます。

DatabaseGenerated 属性については、後のチュートリアルで説明されています。 この属性を使用すると、講座の PK を、データベースによって生成するのではなく、自分で入力できます。

データベース コンテキストの作成

定められたデータ モデルの EF 機能を調整するメイン クラスは、DbContext データベース コンテキスト クラスです。 このクラスは Microsoft.EntityFrameworkCore.DbContext クラスから派生させて作成します。 DbContext 派生クラスによって、データ モデルに含めるエンティティが指定されます。 EF の一部の動作はカスタマイズできます。 このプロジェクトでは、クラスに SchoolContext という名前が付けられています。

プロジェクト フォルダーで、Data という名前のフォルダーを作成します。

Data フォルダーに、次のコードで SchoolContext クラスを作成します。

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 プロパティが作成されます。 EF の用語では:

  • エンティティ セットは通常、データベース テーブルに対応します。
  • エンティティはテーブル内の行に対応します。

DbSet<Enrollment> ステートメントと DbSet<Course> ステートメントは省略することができ、動作は同じです。 EF には、次の理由により暗黙的にこれらが含まれます。

  • Student エンティティでは、Enrollment エンティティが参照されます。
  • Enrollment エンティティでは、Course エンティティが参照されます。

データベースが作成されると、EF によって、DbSet プロパティと同じ名前を持つテーブルが作成されます。 コレクションのプロパティ名は通常、複数形になります。 たとえば、Student ではなく Students。 テーブル名を複数形にするかどうかについては、開発者の間で意見が分かれるでしょう。 これらのチュートリアルでは、DbContext で単数形のテーブル名を指定することにより、既定の動作をオーバーライドします。 そのために、最後の DbSet プロパティの後に、次の強調表示されているコードを追加します。

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");
        }
    }
}

SchoolContext を登録する

ASP.NET Core には、依存関係挿入が含まれています。 EF データベース コンテキストなどのサービスは、アプリの起動時に依存関係の挿入で登録されます。 これらのサービス (MVC コント ローラーなど) を必要とするコンポーネントには、コンストラクターのパラメーターを介してこれらのサービスが提供されます。 コンテキスト インスタンスを取得するコントローラー コンストラクターのコードは、このチュートリアルで後ほど示します。

SchoolContext をサービスとして登録するには、Startup.cs を開き、強調表示されている行を ConfigureServices メソッドに追加します。

using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace ContosoUniversity
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<SchoolContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            services.AddControllersWithViews();
        }

DbContextOptionsBuilder オブジェクトでメソッドが呼び出され、接続文字列の名前がコンテキストに渡されます。 ローカル開発の場合、ASP.NET Core 構成システムによって appsettings.json ファイルから接続文字列が読み取られます。

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": "*"
}

データベース例外フィルターを追加する

次のコードに示すように、ConfigureServicesAddDatabaseDeveloperPageExceptionFilter を追加します。

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<SchoolContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddDatabaseDeveloperPageExceptionFilter();

    services.AddControllersWithViews();
}

AddDatabaseDeveloperPageExceptionFilter により、開発環境で役に立つエラー情報が提供されます。

SQL Server Express LocalDB

この接続文字列によって SQL Server LocalDB が指定されます。 LocalDB は SQL Server Express データベース エンジンの軽量版であり、実稼働ではなく、アプリの開発を意図して設計されています。 LocalDB は要求時に開始され、ユーザー モードで実行されるため、複雑な構成はありません。 既定では、LocalDB は C:/Users/<user> ディレクトリに .mdf DB ファイルを作成します。

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

EF により、空のデータベースが作成されます。 このセクションでは、データベースが作成された後でそれにテスト データを設定するために呼び出されるメソッドを追加します。

EnsureCreated メソッドを使用して、データベースを自動的に作成します。 後のチュートリアルでは、モデル変更の処理方法について説明します。データベースを削除し、再作成するのではなく、Code First Migrations を利用してデータベース スキーマを変更します。

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();

            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            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();
        }
    }
}

上記のコードでは、データベースが存在するかどうかを確認します。

  • データベースが見つからない場合
    • 作成されて、テスト データが読み込まれます。 List<T> コレクションではなく配列にテスト データを読み込み、パフォーマンスを最適化します。
  • データベースが見つかった場合は、何も行われません。

次のコードを使用して Program.cs を更新します。

using ContosoUniversity.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;

namespace ContosoUniversity
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            CreateDbIfNotExists(host);

            host.Run();
        }

        private static 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 logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred creating the DB.");
                }
            }
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

アプリの起動時に、Program.cs によって次のことが行われます。

  • 依存関係挿入コンテナーからデータベース コンテキスト インスタンスを取得します。
  • DbInitializer.Initialize メソッドを呼び出します。
  • 次のコードで示すように、Initialize メソッドが完了したらコンテキストを破棄します。
public static void Main(string[] args)
{
     var host = CreateWebHostBuilder(args).Build();

    using (var scope = host.Services.CreateScope())
    {
        var services = scope.ServiceProvider;
        try
        {
            var context = services.GetRequiredService<SchoolContext>();
            DbInitializer.Initialize(context);
        }
        catch (Exception ex)
        {
            var logger = services.GetRequiredService<ILogger<Program>>();
            logger.LogError(ex, "An error occurred while seeding the database.");
        }
    }

    host.Run();
}

アプリが初めて実行されたときは、データベースが作成され、テスト データが読み込まれます。 データ モデルが変更されるたび:

  • データベースを削除します。
  • シード メソッドを更新し、新しいデータベースで新たに開始します。

後のチュートリアルでは、データ モデルが変更されたら、データベースを削除して作成し直すのではなく、それを更新します。 データ モデルが変更されても、データは失われません。

コントローラーとビューを作成する

Visual Studio のスキャフォールディング エンジンを使用して、MVC コントローラーとビューを追加します。これらにより、EF を使用してクエリが実行され、データが保存されます。

CRUD アクションのメソッドとビューの自動作成は、スキャフォールディングと呼ばれます。

  • ソリューション エクスプローラーで、Controllers フォルダーを右クリックし、[追加] > [新規スキャフォールディング アイテム] を選択します。
  • [スキャフォールディングを追加] ダイアログ ボックスで:
    • [Entity Framework を使用したビューがある MVC コントローラー] を選択します。
    • [追加] をクリックします。 [Entity Framework を使用してビューがある MVC コントローラーを追加する] ダイアログ ボックスが表示されます。Scaffold Student
    • [モデル クラス]Student を選択します。
    • [データ コンテキスト クラス]SchoolContext を選択します。
    • 名前は StudentsController をそのまま選択します。
    • [追加] をクリックします。

Visual Studio スキャフォールディング エンジンにより、StudentsController.cs ファイルと、コントローラーで動作するビューのセット (*.cshtml ファイル) が作成されます。

コントローラーによりコンストラクター パラメーターとして SchoolContext が受け取られることに注意してください。

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

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

ASP.NET Core 依存関係挿入では、SchoolContext のインスタンスがコントローラーに渡されます。 Startup クラスでそれを構成しました。

コントローラーには Index アクション メソッドが含まれます。これはデータベースにあるすべての学生を表示します。 このメソッドはデータベース コンテキスト インスタンスの Students プロパティを読み取り、Students エンティティ セットから学生の一覧を取得します。

public async Task<IActionResult> Index()
{
    return View(await _context.Students.ToListAsync());
}

このコードの非同期プログラミング要素については、チュートリアルで後ほど説明します。

Views/Students/Index.cshtml ビューには、この一覧で表形式で表示されます。

@model IEnumerable<ContosoUniversity.Models.Student>

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

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                <th>
                    @Html.DisplayNameFor(model => model.LastName)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.FirstMidName)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.EnrollmentDate)
                </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>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

CTRL を押しながら F5 を押してプロジェクトを実行するか、メニューで [デバッグ] > [デバッグなしで開始] の順に選択します。

[Students] タブをクリックすると、DbInitializer.Initialize メソッドによって挿入されたテスト データが表示されます。 ブラウザーのウィンドウ幅によって決まることですが、Students タブ リンクはページの一番上に表示されるか、右上隅のナビゲーション アイコンをクリックしないと表示されません。

Contoso University home page narrow

Students Index page

データベースを表示する

アプリが起動すると、DbInitializer.Initialize メソッドによって EnsureCreated が呼び出されます。 EF により、データベースが存在しないことが認識されました。

  • そのため、データベースが作成されました。
  • Initialize メソッドのコードにより、データベースにデータが設定されました。

Visual Studio でデータベースを表示するには、SQL Server オブジェクト エクスプローラー (SSOX) を使用します。

  • Visual Studio の [表示] メニューで [SQL Server オブジェクト エクスプローラー] を選択します。
  • SSOX で、(localdb)\MSSQLLocalDB > [データベース] を選択します。
  • ContosoUniversity1 を選択します。これは、 appsettings.json ファイル内の接続文字列に含まれるデータベース名のエントリです。
  • [テーブル] ノードを展開し、データベースのテーブルを表示します。

Tables in SSOX

Student テーブルを右クリックし、 [データの表示] をクリックして、テーブル内のデータを表示します。

Student table in SSOX

*.mdf および *.ldf データベース ファイルは、C:\Users\<username> フォルダーにあります。

アプリの起動時に実行される初期化メソッドで EnsureCreated が呼び出されるため、次のことができます。

  • Student クラスに対する変更を行います。
  • データベースを削除します。
  • アプリを停止してから開始します。 変更に合わせてデータベースが自動的に再作成されます。

たとえば、Student クラスに EmailAddress プロパティが追加された場合、再作成されたテーブルには新しい EmailAddress 列が含まれます。 ビューには、新しい EmailAddress プロパティは表示されません。

規約

EF によって使用される規約のため、EF で完全なデータベースが作成されるために記述するコードの量は最小限に抑えられます。

  • DbSet プロパティの名前がテーブル名として使用されます。 DbSet プロパティによって参照されないエンティティについては、エンティティ クラス名がテーブル名として使用されます。
  • 列名には、エンティティ プロパティ名が使用されます。
  • ID または classnameID という名前のエンティティ プロパティは、PK プロパティとして認識されます。
  • 名前が <ナビゲーション プロパティ名><PK プロパティ名> であるプロパティは、FK プロパティとして解釈されます。 たとえば、Student ナビゲーション プロパティに対する StudentID (Student エンティティの PK は ID であるため)。 FK プロパティは、<主キー プロパティ名> という名前にすることもできます。 たとえば、EnrollmentID (Enrollment エンティティの PK が EnrollmentID であるため)。

従来の動作をオーバーライドできます。 たとえば、このチュートリアルで先に示したように、テーブル名を明示的に指定できます。 列名と任意のプロパティを、PK または FK として設定できます。

非同期コード

ASP.NET Core と EF Core では、非同期プログラミングが既定のモードです。

Web サーバーでは、利用できるスレッド数に限りがあります。負荷が高い状況では、利用できるスレッドが全部使われる可能性があります。 その場合、スレッドが解放されるまでサーバーは新しい要求を処理できません。 同期コードの場合、たくさんのスレッドが関連付けられていても、I/O の完了を待っているため、実際には何の作業も行っていないということがあります。 非同期コードの場合、あるプロセスが I/O の完了を待っているとき、そのスレッドは解放され、サーバーによって他の要求の処理に利用できます。 結果として、非同期コードの場合、サーバー リソースをより効率的に利用できます。サーバーは、より多くのトラフィックを遅延なく処理できます。

非同期コードは実行時に少量のオーバーヘッドを発生させるが、トラフィックが少ない場合、パフォーマンスに与える影響は無視して構わない程度です。トラフィックが多い場合、相当なパフォーマンス改善が見込まれます。

次のコードでは、asyncTask<T>awaitToListAsync により、コードは非同期的に実行されます。

public async Task<IActionResult> Index()
{
    return View(await _context.Students.ToListAsync());
}
  • キーワード async は、メソッド本文の一部にコールバックを生成し、返された Task<IActionResult> オブジェクトを自動作成するようにコンパイラに伝えます。
  • 戻り値の型 Task<IActionResult> は、進行中の作業と型 IActionResult の結果を表します。
  • キーワード await により、コンパイラはメソッドを 2 つに分割します。 最初の部分は、非同期で開始される操作で終わります。 2 つ目の部分は、操作の完了時に呼び出されるコールバック メソッドに入ります。
  • ToListAsync は、ToList 拡張メソッドの非同期バージョンです。

EF を使用する非同期コードを記述するときに注意すべき点:

  • クエリやコマンドをデータベースに送信するステートメントのみが非同期で実行されます。 たとえば、ToListAsyncSingleOrDefaultAsyncSaveChangesAsync などです。 var students = context.Students.Where(s => s.LastName == "Davolio") など、IQueryable を変更するだけのステートメントは含まれません。
  • EF コンテキストはスレッド セーフではありません。複数の操作を並列実行しないでください。 非同期 EF メソッドを呼び出すとき、await キーワードを常に使用します。
  • 非同期コードのパフォーマンス上の利点を利用するには、使用されているライブラリ パッケージで、クエリがデータベースに送信される EF のメソッドが呼び出されている場合は、それらでも非同期を使用するようにします。

.NET の非同期プログラミングについては、「非同期の概要」を参照してください。

フェッチされるエンティティを制限する

クエリから返されるエンティティの数の制限については、「パフォーマンスに関する考慮事項」を参照してください。

Entity Framework Core の SQL ログ

一般的に、ログの構成は appsettings.{Environment}.json ファイルの Logging セクションで指定されます。 SQL ステートメントをログに記録するには、appsettings.Development.json ファイルに "Microsoft.EntityFrameworkCore.Database.Command": "Information" を追加します。

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

上記の JSON では、SQL ステートメントがコマンド ラインと Visual Studio 出力ウィンドウに表示されます。

詳細については、「NET Core と ASP.NET Core でのログ記録」とこの GitHub イシューを参照してください。

基本的な CRUD (作成、読み取り、更新、削除) 操作の実行方法を学習するには、次のチュートリアルに進んでください。

このチュートリアルでは、ASP.NET Core MVC と Entity Framework Core のコントローラーとビューについて説明します。 Razor Pages は代替プログラミング モデルです。 新しい開発では、コントローラーやビューを使う MVC よりも Razor Pages を使うことをお勧めします。 このチュートリアルの Razor Pages バージョンを参照してください。 それぞれのチュートリアルには、もう一方では説明されない内容が含まれています。

この MVC のチュートリアルに含まれ、Razor Pages のチュートリアルには含まれていないこと:

  • データ モデルで継承を実装する
  • 生 SQL クエリを実行する
  • 動的な LINQ を使ってコードを簡略化する

Razor Pages のチュートリアルに含まれ、このチュートリアルには含まれていないこと:

  • Select メソッドを使って関連データを読み込む
  • EF のベスト プラクティス。

Contoso University のサンプル Web アプリケーションでは、Entity Framework (EF) Core 2.2 と Visual Studio 2017 または 2019 を使用して ASP.NET Core 2.2 MVC Web アプリケーションを作成する方法を示します。

このチュートリアルは、ASP.NET Core 3.1 用に更新されていません。 ASP.NET Core 5.0 用に更新されています。

サンプル アプリケーションは架空の Contoso University の Web サイトです。 学生の受け付け、講座の作成、講師の割り当てなどの機能が含まれています。 これは、Contoso University のサンプル アプリケーションを一から作成する方法を説明するチュートリアル シリーズの 1 回目です。

必須コンポーネント

  • .NET Core SDK 2.2
  • Visual Studio 2019 と次のワークロード:
    • ASP.NET および Web の開発ワークロード
    • .NET Core クロスプラットフォームの開発ワークロード

トラブルシューティング

解決できない問題に遭遇した場合、通常、完成済みのプロジェクトと自分のコードを比較することで解決策がわかります。 一般的なエラーとその解決方法の一覧については、このシリーズの最後のチュートリアルにあるトラブルシューティングのセクションをご覧ください。 そこで必要な答えが見つからない場合、StackOverflow.com で ASP.NET Core または EF Core に関する質問を投稿できます。

ヒント

これは 10 回のチュートリアルからなるシリーズであり、いずれの回も前のチュートリアルを基盤にしています。 チュートリアルが完了したら、毎回、プロジェクトのコピーを保存するようお勧めします。 問題に遭遇したとき、前のチュートリアルから始めることができます。シリーズ全体の始めまで戻る必要がありません。

Contoso University Web アプリ

一連のチュートリアルで作成するアプリケーションは、簡単な大学向け Web サイトです。

ユーザーは学生、講座、講師の情報を見たり、更新したりできます。 次のような画面をこれから作成します。

Students Index page

Students Edit page

Web アプリを作成する

  • Visual Studio を開きます。

  • [ファイル] メニューで、 [新規作成] > [プロジェクト] の順に選びます。

  • 左側のウィンドウで、[インストール済み] > [Visual C#] > [Web] の順に選択します。

  • [ASP.NET Core Web アプリケーション] プロジェクト テンプレートを選択します。

  • 名前に「ContosoUniversity」と入力し、 [OK] をクリックします。

    New Project dialog

  • [新しい ASP.NET Core Web アプリケーション] ダイアログが表示されるのを待ちます。

  • [.NET Core][ASP.NET Core 2.2] 、および [Web アプリケーション (モデル ビュー コントローラー)] テンプレートを選択します。

  • [認証][認証なし] が設定されていることを確認してください。

  • [OK] を選択します。

    New ASP.NET Core Project dialog

サイトのスタイルを設定する

簡単な変更をいくつか行い、サイトのメニュー、レイアウト、ホーム ページを決めます。

Views/Shared/_Layout.cshtml を開き、次の変更を行います。

  • "ContosoUniversity" をすべて "Contoso University" に変更します。 これは 3 回出てきます。

  • メニュー エントリとして「About」、「Students」、「Courses」、「Instructors」、「Departments」を追加し、「Privacy」メニュー エントリを削除します。

変更が強調表示されます。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - Contoso University</title>

    <environment include="Development">
        <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    </environment>
    <environment exclude="Development">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/css/bootstrap.css"
              asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.css"
              asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute"
              crossorigin="anonymous"
              integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE="/>
    </environment>
    <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">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Contoso University</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
                    <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>
    <div class="container">
        <partial name="_CookieConsentPartial" />
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

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

    <environment include="Development">
        <script src="~/lib/jquery/dist/jquery.js"></script>
        <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
    </environment>
    <environment exclude="Development">
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.js"
                asp-fallback-src="~/lib/jquery/dist/jquery.js"
                asp-fallback-test="window.jQuery"
                crossorigin="anonymous"
                integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=">
        </script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/js/bootstrap.bundle.js"
                asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"
                asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
                crossorigin="anonymous"
                integrity="sha256-E/V4cWE4qvAeO5MOhjtGtqDzPndRO1LBk8lJ/PR7CA4=">
        </script>
    </environment>
    <script src="~/js/site.js" asp-append-version="true"></script>

    @RenderSection("Scripts", required: false)
</body>
</html>

Views/Home/Index.cshtml で、ファイルの中身を次のコードに変更し、ASP.NET と MVC に関するテキストをこのアプリケーションに関するテキストに置き換えます。

@{
    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/cu-final">See project source code &raquo;</a></p>
    </div>
</div>

CTRL を押しながら F5 を押してプロジェクトを実行するか、メニューで [デバッグ] > [デバッグなしで開始] の順に選択します。 一連のチュートリアルで作成するページのホーム ページとタブが表示されます。

Contoso University home page

EF Core の NuGet パッケージについて

プロジェクトに EF Core サポートを追加するには、対象とするデータベース プロバイダーをインストールします。 このチュートリアルでは SQL Server を使用します。プロバイダー パッケージは Microsoft.EntityFrameworkCore.SqlServer です。 このパッケージは Microsoft.AspNetCore.App メタパッケージに含まれているので、パッケージを参照する必要はありません。

EF SQL Server パッケージとその依存関係 (Microsoft.EntityFrameworkCoreMicrosoft.EntityFrameworkCore.Relational) により、EF のランタイム サポートが提供されます。 後の移行チュートリアルでツール パッケージを追加します。

Entity Framework Core で利用できるその他のデータベース プロバイダーに関しては、「データベース プロバイダー」を参照してください。

データ モデルの作成

次に、Contoso University アプリケーションのエンティティ クラスを作成します。 次の 3 つのエンティティから始めます。

Course-Enrollment-Student data model diagram

Student エンティティと Enrollment エンティティの間に一対多の関係があり、Course エンティティと Enrollment エンティティの間に一対多の関係があります。 言い換えると、1 人の学生をさまざまな講座に登録し、1 つの講座にたくさんの学生を登録できます。

次のセクションでは、エンティティごとにクラスを作成します。

Student エンティティ

Student entity diagram

[Models] フォルダーで、Student.cs という名前のクラス ファイルを作成し、テンプレート コードを次のコードに変更します。

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

ID プロパティは、このクラスに相当するデータベース テーブルの主キー列になります。 既定では、Entity Framework は、ID または classnameID という名前のプロパティを主キーとして解釈します。

Enrollments プロパティはナビゲーション プロパティです。 ナビゲーション プロパティには、このエンティティに関連する他のエンティティが含まれます。 この例では、Student entityEnrollments プロパティで、その Student エンティティに関連するすべての Enrollment エンティティが保持されます。 つまり、データベースの Student 行に関連する Enrollment 行 (StudentID 外部キー列にその学生の主キー値が含まれる行) が 2 つある場合、その Student エンティティの Enrollments ナビゲーション プロパティには、それら 2 つの Enrollment エンティティが含まれます。

ナビゲーション プロパティに複数のエンティティが含まれる場合 (多対多または一対多の関係で)、その型はリストにする必要があります。ICollection<T> のように、エンティティを追加、削除、更新できるリストです。 ICollection<T>、または List<T>HashSet<T> などの型を指定することができます。 ICollection<T> を指定した場合、EF では既定で HashSet<T> コレクションが作成されます。

Enrollment エンティティ

Enrollment entity diagram

[Models] フォルダーで、Enrollment.cs を作成し、既存のコードを次のコードに変更します。

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

EnrollmentID プロパティは主キーになります。このエンティティは、Student エンティティと同様に、ID ではなく classnameID パターンを使用します。 通常、パターンを 1 つ選択し、データ モデル全体でそれを使用します。 ここのバリエーションから、いずれのパターンも利用できることがわかります。 後のチュートリアルでは、クラス名なしの ID を利用し、データ モデルに継承を簡単に実装する方法を学習します。

Grade プロパティは enum です。 Grade の型宣言の後の疑問符は、Grade プロパティが null 許容であることを示します。 null という成績は、0 の成績とは異なります。null は成績がわからないことか、まだ評価されていないことを意味します。

StudentID プロパティは外部キーです。それに対応するナビゲーション プロパティは Student です。 Enrollment エンティティは 1 つの Student エンティティに関連付けられており、1 つの Student エンティティだけを保持できます (先に見た、複数の Enrollment エンティティを保持できる Student.Enrollments ナビゲーション プロパティとは異なります)。

CourseID プロパティは外部キーです。それに対応するナビゲーション プロパティは Course です。 Enrollment エンティティは 1 つの Course エンティティに関連付けられます。

Entity Framework は <navigation property name><primary key property name> という名前が付いている場合、プロパティを外部キー プロパティとして解釈します。たとえば、Student ナビゲーション プロパティの StudentID です。Student エンティティの主キーが ID であるためです。 外部キーにも <primary key property name> という単純な名前を付けることができます。たとえば、CourseID です。Course エンティティの主キーが CourseID であるためです。

Course エンティティ

Course entity diagram

[Models] フォルダーで、Course.cs を作成し、既存のコードを次のコードに変更します。

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

Enrollments プロパティはナビゲーション プロパティです。 1 つの Course エンティティにたくさんの Enrollment エンティティを関連付けることができます。

DatabaseGenerated 属性については、このシリーズの後のチュートリアルで詳しく学習します。 基本的に、この属性によって、講座の主キーをデータベースに生成させず、自分で入力できるようになります。

データベース コンテキストの作成

所与のデータ モデルの Entity Framework 機能を調整するメイン クラスは、データベース コンテキスト クラスです。 このクラスは、Microsoft.EntityFrameworkCore.DbContext クラスから派生させて作成します。 自分のコードでは、データ モデルに含めるエンティティを自分で指定します。 Entity Framework の特定の動作をカスタマイズすることもできます。 このプロジェクトでは、クラスに SchoolContext という名前が付けられています。

プロジェクト フォルダーで、Data という名前のフォルダーを作成します。

[Data] フォルダーで、SchoolContext.cs という名前の新しいクラス ファイルを作成し、テンプレート コードを次のコードに変更します。

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 プロパティが作成されます。 Entity Framework の用語では、エンティティ セットは通常はデータベース テーブルに対応し、エンティティはテーブルの行に対応します。

DbSet<Enrollment> ステートメントと DbSet<Course> ステートメントは省略しても同じ動作をします。 Entity Framework にはそれらが暗黙的に含まれることがあります。Student エンティティが Enrollment エンティティを参照し、Enrollment エンティティが Course エンティティを参照するためです。

データベースが作成されると、EF によって、DbSet プロパティと同じ名前を持つテーブルが作成されます。 一般的にコレクションのプロパティ名は複数形 (Student ではなく、Students) ですが、テーブル名を複数にするかどうかについては、開発者の間で意見が分かれています。 このチュートリアル シリーズでは、DbContext に単数のテーブル名を指定して既定の動作をオーバーライドします。 そのために、最後の DbSet プロパティの後に、次の強調表示されているコードを追加します。

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");
        }
    }
}

コンパイラ エラーのチェックとしてプロジェクトをビルドします。

SchoolContext を登録する

ASP.NET Core は既定で依存関係の挿入を実装します。 サービス (EF データベース コンテキストなど) は、アプリケーションの起動時に依存関係の挿入に登録されます。 これらのサービス (MVC コント ローラーなど) を必要とするコンポーネントには、コンストラクターのパラメーターを介してこれらのサービスが指定されます。 このチュートリアルの後半で、コンテキスト インスタンスを取得するコントローラー コンストラクター コードが登場します。

SchoolContext をサービスとして登録するには、Startup.cs を開き、強調表示されている行を ConfigureServices メソッドに追加します。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    services.AddDbContext<SchoolContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddMvc();
}

DbContextOptionsBuilder オブジェクトでメソッドが呼び出され、接続文字列の名前がコンテキストに渡されます。 ローカル開発の場合、ASP.NET Core 構成システムによって appsettings.json ファイルから接続文字列が読み取られます。

名前空間の ContosoUniversity.DataMicrosoft.EntityFrameworkCore に対して using ステートメントを追加し、プロジェクトをビルドします。

using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Http;

appsettings.json ファイルを開き、次の例で示されているように接続文字列を追加します。

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity1;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Warning"
    }
  }
}

SQL Server Express LocalDB

この接続文字列によって SQL Server LocalDB データベースが指定されます。 LocalDB は SQL Server Express データベース エンジンの軽量版であり、実稼働ではなく、アプリケーションの開発を意図して設計されています。 LocalDB は要求時に開始され、ユーザー モードで実行されるため、複雑な構成はありません。 既定では、LocalDB は C:/Users/<user> ディレクトリに .mdf データベース ファイルを作成します。

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

Entity Framework によって空のデータベースが自動的に作成されます。 このセクションでは、テスト データを入力する目的で、データベースの作成後に呼び出されるメソッドを記述します。

ここでは、データベースを自動的に作成する EnsureCreated メソッドを使用します。 後のチュートリアルでは、モデル変更の処理方法について学習します。データベースを削除し、再作成するのではなく、Code First Migrations を利用してデータベース スキーマを変更します。

[データ] フォルダーで DbInitializer.cs という名前の新しいクラス ファイルを作成し、テンプレート コードを次のコードに変更します。このコードにより、必要なときにデータベースが作成され、新しいデータベースにテスト データが読み込まれます。

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

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

            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            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();
        }
    }
}

このコードはデータベースに学生が存在するかどうかを確認し、存在しない場合、そのデータベースは新しく、テスト データを入力する必要があると見なします。 List<T> コレクションではなく配列にテスト データを読み込み、パフォーマンスを最適化します。

Program.cs で、アプリケーションの起動時に次を実行するように Main メソッドを変更します。

  • 依存関係挿入コンテナーからデータベース コンテキスト インスタンスを取得します。
  • seed メソッドを呼び出し、コンテキストを渡します。
  • seed メソッドが完了したら、コンテキストを破棄します。
using ContosoUniversity.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;

namespace ContosoUniversity
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            CreateDbIfNotExists(host);

            host.Run();
        }

        private static 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 logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred creating the DB.");
                }
            }
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

アプリケーションを初めて実行すると、データベースが作成され、テスト データが設定されます。 データ モデルを変更するたび:

  • データベースを削除します。
  • シード メソッドを更新し、同じようにして新しいデータベースで新たに開始します。

後のチュートリアルでは、データ モデルが変わったとき、データベースを削除して作り直すのではなく、修正する方法について学習します。

コントローラーとビューを作成する

このセクションでは、Visual Studio のスキャフォールディング エンジンを使用して、MVC のコントローラーとビューを追加します。それらにより、EF を使用して、クエリが実行され、データが保存されます。

CRUD アクションのメソッドとビューの自動作成は、スキャフォールディングと言います。 一般的には生成されたコードは修正しないのに対し、スキャフォールディングされたコードを開始点として独自の要件に合うように変更できるという点で、スキャフォールディングはコード生成と異なります。 生成されたコードをカスタマイズする必要があるとき、部分クラスを利用するか、状況が変わったときにコードを再生成します。

  • ソリューション エクスプローラーControllers フォルダーを右クリックし、[追加] > [スキャフォールディングされた新しい項目] の順に選択します。
  • [スキャフォールディングを追加] ダイアログ ボックスで:
    • [Entity Framework を使用したビューがある MVC コントローラー] を選択します。
    • [追加] をクリックします。 [Entity Framework を使用してビューがある MVC コントローラーを追加する] ダイアログ ボックスが表示されます。Scaffold Student
    • [モデル クラス][Student] を選択します。
    • [データ コンテキスト クラス][SchoolContext] を選択します。
    • 名前は StudentsController をそのまま選択します。
    • [追加] をクリックします。

Visual Studio スキャフォールディング エンジンにより、StudentsController.cs ファイルと、コントローラーで動作するビューのセット (.cshtml ファイル) が作成されます。

コントローラーによりコンストラクター パラメーターとして SchoolContext が受け取られることに注意してください。

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

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

ASP.NET Core 依存関係挿入では、SchoolContext のインスタンスがコントローラーに渡されます。 これは、Startup.cs ファイルで構成しました。

コントローラーには Index アクション メソッドが含まれます。これはデータベースにあるすべての学生を表示します。 このメソッドはデータベース コンテキスト インスタンスの Students プロパティを読み取り、Students エンティティ セットから学生の一覧を取得します。

public async Task<IActionResult> Index()
{
    return View(await _context.Students.ToListAsync());
}

このコードの非同期プログラミング要素については、チュートリアルで後ほど学習します。

Views/Students/Index.cshtml ビューには、この一覧で表形式で表示されます。

@model IEnumerable<ContosoUniversity.Models.Student>

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

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                <th>
                    @Html.DisplayNameFor(model => model.LastName)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.FirstMidName)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.EnrollmentDate)
                </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>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

CTRL を押しながら F5 を押してプロジェクトを実行するか、メニューで [デバッグ] > [デバッグなしで開始] の順に選択します。

[Students] タブをクリックすると、DbInitializer.Initialize メソッドによって挿入されたテスト データが表示されます。 ブラウザーのウィンドウ幅によって決まることですが、Students タブ リンクはページの一番上に表示されるか、右上隅のナビゲーション アイコンをクリックしないと表示されません。

Contoso University home page narrow

Students Index page

データベースを表示する

アプリケーションを起動する葉、DbInitializer.Initialize メソッドが EnsureCreated を呼び出します。 EF はデータベースがないことを認識し、作成します。Initialize メソッド コードの残りの部分により、データベースにデータが入力されます。 SQL Server Object Explorer (SSOX) を利用し、Visual Studio でデータベースを表示できます。

ブラウザーを閉じます。

SSOX ウィンドウがまだ開いていない場合、Visual Studio の [表示] メニューから選択します。

SSOX で (localdb)\MSSQLLocalDB > [データベース] をクリックし、appsettings.json ファイルの接続文字列にあるデータベース名のエントリをクリックします。

[テーブル] ノードを展開し、データベースのテーブルを表示します。

Tables in SSOX

[Student] テーブルを右クリックし、 [データの表示] をクリックすると、作成された列とテーブルに挿入された行が表示されます。

Student table in SSOX

データベース ファイルの .mdf.ldfC:\Users<ユーザー名> フォルダーにあります。

アプリの起動時に実行される初期化子メソッドで EnsureCreated を呼び出すため、Student クラスを変更し、データベースを削除し、アプリケーションを再実行できます。変更に合わせてデータベースが自動的に再作成されます。 たとえば、Student クラスに EmailAddress プロパティを追加する場合、再作成されたテーブルに新しい EmailAddress 列が表示されます。

規約

規約を利用することや Entity Framework が想定を行うことにより、Entity Framework が完全なデータベースを自動作成するために記述しなければならないコードの量が最小限に抑えられます。

  • DbSet プロパティの名前がテーブル名として使用されます。 DbSet プロパティによって参照されないエンティティについては、エンティティ クラス名がテーブル名として使用されます。
  • 列名には、エンティティ プロパティ名が使用されます。
  • ID または classnameID という名前が付けられているエンティティ プロパティは主キーのプロパティとして認識されます。
  • <ナビゲーション プロパティ名><主キー プロパティ名> という名前 (たとえば、Student ナビゲーション プロパティの場合、Student エンティティの主キーが ID なので、StudentID となります) が付いている場合、プロパティは外部キー プロパティとして解釈されます。 外部キー プロパティにも <主キー プロパティ名> の単純な名前を付けることができます (たとえば、Enrollment エンティティの主キーは EnrollmentID なので EnrollmentID)。

従来の動作をオーバーライドできます。 たとえば、このチュートリアルで先に見たように、テーブル名を明示的に指定できます。 また、列名を設定し、任意のプロパティを主キーまたは外部キーとして設定できます。これについては、このシリーズの後のチュートリアルで学習します。

非同期コード

ASP.NET Core と EF Core では、非同期プログラミングが既定のモードです。

Web サーバーでは、利用できるスレッド数に限りがあります。負荷が高い状況では、利用できるスレッドが全部使われる可能性があります。 その場合、スレッドが解放されるまでサーバーは新しい要求を処理できません。 同期コードの場合、たくさんのスレッドが関連付けられていても、I/O の完了を待っているため、実際には何の作業も行っていないということがあります。 非同期コードの場合、あるプロセスが I/O の完了を待っているとき、そのスレッドは解放され、サーバーによって他の要求の処理に利用できます。 結果として、非同期コードの場合、サーバー リソースをより効率的に利用できます。サーバーは、より多くのトラフィックを遅延なく処理できます。

非同期コードは実行時に少量のオーバーヘッドを発生させるが、トラフィックが少ない場合、パフォーマンスに与える影響は無視して構わない程度です。トラフィックが多い場合、相当なパフォーマンス改善が見込まれます。

次のコードでは、キーワード async、戻り値 Task<T>、キーワード await、メソッド ToListAsync によりコードの実行が非同期になります。

public async Task<IActionResult> Index()
{
    return View(await _context.Students.ToListAsync());
}
  • キーワード async は、メソッド本文の一部にコールバックを生成し、返された Task<IActionResult> オブジェクトを自動作成するようにコンパイラに伝えます。
  • 戻り値の型 Task<IActionResult> は、進行中の作業と型 IActionResult の結果を表します。
  • キーワード await により、コンパイラはメソッドを 2 つに分割します。 最初の部分は、非同期で開始される操作で終わります。 2 つ目の部分は、操作の完了時に呼び出されるコールバック メソッドに入ります。
  • ToListAsync は、ToList 拡張メソッドの非同期バージョンです。

Entity Framework を利用する非同期コードの記述で注意すべき点:

  • クエリやコマンドをデータベースに送信するステートメントのみが非同期で実行されます。 たとえば、ToListAsyncSingleOrDefaultAsyncSaveChangesAsync などです。 var students = context.Students.Where(s => s.LastName == "Davolio") など、IQueryable を変更するだけのステートメントは含まれません。
  • EF コンテキストはスレッド セーフではありません。複数の操作を並列実行しないでください。 非同期 EF メソッドを呼び出すとき、await キーワードを常に使用します。
  • 非同期コードのパフォーマンス上の利点を最大限に活用する場合、(ページングなどのために) ライブラリ パッケージを利用しているのであれば、それがクエリをデータベースに送信させる Entity Framework メソッドを呼び出す場合、非同期を利用する必要があります。

.NET の非同期プログラミングについては、「非同期の概要」を参照してください。

次のステップ

基本的な CRUD (作成、読み取り、更新、削除) 操作の実行方法を学習するには、次のチュートリアルに進んでください。