教學課程:開始在 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 的最佳做法。

Contoso 大學的範例 Web 應用程式將示範如何以 Entity Framework (EF) Core 和 Visual Studio 來建立 ASP.NET Core MVC Web 應用程式。

這個範例應用程式是虛構的 Contoso 大學網站。 其中包括的功能有學生入學許可、課程建立、教師指派。 這是說明如何建立 Contoso 大學範例應用程式教學課程系列中的第一頁。

必要條件

此教學課程尚未升級至 ASP.NET Core 6 或更新版本。 如果您建立以 ASP.NET Core 6 或更新版本為目標的專案,本教學課程的指示將無法正確運作。 例如,ASP.NET Core 6 和更新版本的 Web 範本會使用 最小的裝載模型,以統 Startup.cs 一和 Program.cs 放入單 Program.cs 一檔案。

.NET 6 中出現的另一個差異是 NRT (可為 Null 的參考型別) 功能。 專案範本預設會啟用此功能。 諸如 EF 在 .NET 6 中將某個屬性視為必要,但該屬性在 .NET 5 中卻可為 Null 的這類情形,就會發生問題。 例如,除非 Enrollments 屬性變成可為 Null 或 asp-validation-summary 協助程式標籤從 ModelOnly 變更為 All,否則 [建立學生] 頁面就會失敗,而且不會出現失敗訊息。

我們建議您針對本教學課程安裝並使用 .NET 5 SDK。 在本教學課程更新之前,若需如何使用 Entity Framework 搭配 ASP.NET Core 6 或更新版本的資訊,請參閱ASP.NET Core 中的 Razor 頁面與 Entity Framework Core 教學課程 - 1/8

資料庫引擎

Visual Studio 說明會使用 SQL Server LocalDB,它是一種只在 Windows 上執行的 SQL Server Express 版本。

解決及疑難排解問題

如果您遭遇無法解決的問題,將您的程式碼與已完成的專案作比較,通常可以找到解答。 如需常見錯誤和如何解決這些問題的清單,請參閱 本系列最後一個教學課程中的疑難排解一節。 如果您在那裡找不到所需的資訊,可以在 StackOverflow.com 上張貼關於 ASP.NET CoreEF Core 的問題。

提示

這是 10 個教學的系列課程,當中的每一個課程都是建置於先前教學課程的成果上。 成功完成每一個教學課程後,請儲存專案的複本。 如果您遇到問題,您可以從上一個教學課程來重新開始,而不需從系列的一開始從頭來過。

Contoso 大學 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 CoreASP.NET Core 5.0
    2. ASP.NET Core Web 應用程式 (Model-View-Controller)
    3. 建立New ASP.NET Core Project dialog

設定網站樣式

簡單幾項基本變更就能設定網站選單、配置和首頁。

開啟 Views/Shared/_Layout.cshtml,然後進行下列變更:

  • 將每個出現 ContosoUniversity 之處變更為 Contoso University。 共有三個發生次數。
  • 為 [About]、[Students]、[Courses]、[Instructors] 及 [Departments] 新增功能表項目,並刪除 [Privacy]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

上述實體具有下列關聯性:

  • StudentEnrollment 實體之間具有一對多關聯性。 一位學生可註冊任何數目的課程。
  • CourseEnrollment 實體之間具有一對多關聯性。 一堂課程可以讓任意數目的學生註冊。

在下列一節中,將為這些實體建立各自的類別。

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 預設會將名為 IDclassnameID 的屬性解譯為主索引鍵。 例如,PK 可以命名為 StudentID,而不是 ID

Enrollments 屬性為導覽屬性。 導覽屬性會保留與此實體相關的其他實體。 Student 實體的 Enrollments 屬性:

  • 包含與該 Student 實體相關的所有 Enrollment 實體。
  • 如果資料庫中的特定 Student 資料列具有兩個相關的 Enrollment 資料列:
    • Student 實體的 Enrollments 導覽屬性會包含這兩個 Enrollment 實體。

Enrollment 資料列在 StudentID 外部索引鍵 (FK) 資料行中會包含學生的 PK 值。

若導覽屬性中可以保留多個實體:

  • 該類型必須是清單,例如 ICollection<T>List<T>HashSet<T>
  • 您可以新增、刪除和更新實體。

多對多和一對多導覽關聯性可以包含多個實體。 若使用 ICollection<T>,EF Core 預設將建立 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。 這個實體會使用 classnameID 模式,而非本身的 IDStudent 實體會使用 ID 模式。 有些開發人員偏好在整個資料模型中使用單一模式。 在本教學課程中,變化說明可以使用任一模式。 稍後的教學課程會說明使用沒有 classname 的 ID,能夠如何讓資料模型中的實作繼承變得更簡單。

Grade 屬性為 enum。 在 Grade 型別宣告之後的 ? 表示 Grade 屬性可為 Null。 成績為 null 與成績為零不同。 null 代表成績未知或尚未指派。

StudentID 屬性是外部索引鍵 (FK),對應的導覽屬性是 Student。 一個 Enrollment 實體會與一個 Student 實體建立關聯,因此該屬性僅能保留單一 Student 實體。 這與 Student.Enrollments 導覽屬性不同,可以保留多個 Enrollment 實體。

CourseID 屬性是 FK,對應的導覽屬性是 Course。 一個 Enrollment 實體與一個 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 屬性為導覽屬性。 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 屬性名稱相同的資料表。 集合的屬性名稱通常是複數。 例如,Students 而非 Student。 針對是否要複數化資料表名稱,開發人員並沒有共識。 在此系列教學課程中,預設行為將由 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": "*"
}

新增資料庫例外狀況篩選條件

AddDatabaseDeveloperPageExceptionFilter 新增至 ConfigureServices,如下列程式碼所示:

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 Database Engine,旨在用於應用程序開發,而不是生產用途。 LocalDB 會依需求啟動,並以使用者模式執行,因此沒有複雜的組態。 LocalDB 預設會在 C:/Users/<user> 目錄中建立 .mdf 資料庫檔案。

使用測試資料將 DB 初始化

EF 會建立空白資料庫。 在本節中,我們將會新增一個方法,而該方法會在資料庫建立之後呼叫,以將測試資料填入資料庫。

EnsureCreated 方法將會用來自動建立資料庫。 在稍後的教學課程中,您將會了解到如何使用 Code First 移轉變更資料庫結構描述,而非卸除並重新建立資料庫,來處理模型的變更。

在 [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 中的 Scaffolding 引擎來新增使用 EF 查詢和儲存資料的 MVC 控制器及檢視。

自動建立 CRUD 動作方法和檢視稱為 Scaffolding。

  • 在 [方案總管] 中,以滑鼠右鍵按一下 Controllers 資料夾,然後選取 [新增] > [新增 Scaffold 項目]
  • 在 [新增 Scaffold] 對話方塊中:
    • 選取 [使用 Entity Framework 執行檢視的 MVC 控制器]
    • 按一下新增。 [新增使用 Entity Framework 執行檢視的 MVC 控制器] 對話方塊隨即出現:Scaffold Student
    • 在 [模型類別] 中,選取 [Student]
    • 在 [資料內容類別] 中,選取 [SchoolContext]
    • 接受預設的 StudentsController 作為名稱。
    • 按一下新增

Visual Studio Scaffolding 引擎會建立 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 方法程式碼會以資料填入資料庫。

使用 [SQL Server 物件總管] (SSOX) 以在 Visual Studio 中檢視資料庫:

  • 從 Visual Studio 中的 [檢視] 功能表選取 [SQL Server 物件總管]
  • 在 SSOX 中,選取 [(localdb)\MSSQLLocalDB] > [資料庫]
  • 選取 ContosoUniversity1,這是 appsettings.json 檔案中連接字串內資料庫名稱的項目。
  • 展開 [資料表] 節點以查看資料庫中的資料表。

Tables in SSOX

以滑鼠右鍵按一下 [Student] 資料表,然後按一下 [檢視資料] 以查看資料表中的資料。

Student table in SSOX

*.mdf*.ldf 資料庫檔案皆位於 C:\Users\<使用者名稱> 資料夾中。

因為應用程式啟動時執行的初始化運算式方法會呼叫 EnsureCreated,所以您可以:

  • Student 類別進行變更。
  • 刪除資料庫。
  • 停止,然後啟動應用程式。 資料庫會自動重新建立,以符合變更。

例如,若將 EmailAddress 屬性新增到 Student 類別,重新建立的資料表中應會出現新的 EmailAddress 資料行。 檢視不會顯示新的 EmailAddress 屬性。

慣例

為了讓 EF 建立完整資料庫所需撰寫的程式碼數量非常少,這是因為 EF 使用慣例的緣故:

  • DbSet 屬性的名稱會用於資料表名稱。 針對 DbSet 屬性並未參考的實體,實體類別名稱會用於資料表名稱。
  • 實體屬性名稱會用於資料行名稱。
  • 名為 IDclassnameID 的實體屬性會辨識為 PK 屬性。
  • 如果屬性命名為 <導覽屬性名稱><PK 屬性名稱>,則該屬性將解譯為 PK 屬性。 例如,Student 導覽屬性的 StudentID,因為 Student 實體的 PK 是 ID。 FK 屬性也可以命名為 <主索引鍵屬性名稱>。 例如 EnrollmentID,因為 Enrollment 實體的 PK 是 EnrollmentID

慣例行為可以被覆寫。 例如,可以明確指定資料表名稱,如稍早在本教學課程中所示。 資料行名稱和任何屬性都可以設定為 PK 或 FK。

非同步程式碼

非同步程式設計是預設的 ASP.NET Core 和 EF Core 模式。

網頁伺服器的可用執行緒數量有限,而且在高負載情況下,可能會使用所有可用的執行緒。 發生此情況時,伺服器將無法處理新的要求,直到執行緒空出來。 使用同步程式碼,許多執行緒可能在實際上並未執行任何工作時受到占用,原因是在等候 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 關鍵字會使編譯器將方法分割為兩部分。 第一個部分會與非同步啟動的作業一同結束。 第二個部分則會放入作業完成時呼叫的回呼方法中。
  • ToListAsyncToList 擴充方法的非同步版本。

若要撰寫使用 EF 的非同步程式碼,請注意下列事項:

  • 只有讓查詢或命令傳送至資料庫的陳述式,會以非同步方式執行。 其中包含,例如 ToListAsyncSingleOrDefaultAsync,以及 SaveChangesAsync。 其中不包含,例如:僅變更 IQueryable 的陳述式,例如 var students = context.Students.Where(s => s.LastName == "Davolio")
  • EF 內容在執行緒中並不安全:不要嘗試執行多個平行作業。 當您呼叫任何 async EF 方法時,請一律使用 await 關鍵字。
  • 若要充分利用非同步程式碼帶來的效能優點,若使用的任何程式庫封裝會呼叫任何可能會傳送查詢到資料庫的 EF 方法,請確保該程式庫封裝也是使用非同步程式碼。

如需在 .NET 中非同步程式設計的詳細資訊,請參閱非同步總覽

限制擷取的實體

如需限制從查詢傳回的實體數目資訊,請參閱 效能考量

Entity Framework Core 的 SQL 記錄

記錄組態通常是由 appsettings.{Environment}.json 檔案的 Logging 區段所提供。 若要記錄 SQL 陳述式,請將 "Microsoft.EntityFrameworkCore.Database.Command": "Information" 新增至 appsettings.Development.json 檔案:

{
  "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 大學範例 Web 應用程式示範如何使用 Entity Framework (EF) Core 2.2 和 Visual Studio 2019 來建立 ASP.NET Core 2.2 MVC Web 應用程式。

此教學課程尚未升級至 ASP.NET Core 3.1。 其已針對 ASP.NET Core 5.0 進行更新。

這個範例應用程式是虛構的 Contoso 大學網站。 其中包括的功能有學生入學許可、課程建立、教師指派。 這是說明如何從零開始建立 Contoso 大學範例應用程式教學課程系列中的第一頁。

必要條件

疑難排解

如果您遭遇無法解決的問題,將您的程式碼與已完成的專案作比較,通常可以找到解答。 如需常見錯誤和如何解決這些問題的清單,請參閱 本系列最後一個教學課程中的疑難排解一節。 如果您在那裡找不到所需的資訊,可以在 StackOverflow.com 上張貼關於 ASP.NET CoreEF Core 的問題。

提示

這是 10 個教學的系列課程,當中的每一個課程都是建置於先前教學課程的成果上。 成功完成每一個教學課程後,請儲存專案的複本。 如果您遇到問題,您可以從上一個教學課程來重新開始,而不需從系列的一開始從頭來過。

Contoso 大學 Web 應用程式

您在這些教學課程中會建置的應用程式為一個簡單的大學網站。

使用者可以檢視和更新學生、課程和教師資訊。 以下是您會建立的幾個畫面。

Students Index page

Students Edit page

建立 Web 應用程式

  • 開啟 Visual Studio。

  • 從 [檔案] 功能表選取[新增] > [專案]

  • 從左側窗格中,選取 [已安裝] > [Visual C#] > [Web]

  • 選取 [ASP.NET Core Web 應用程式] 專案範本。

  • 輸入 ContosoUniversity 作為名稱,然後按一下 [確定]

    New Project dialog

  • 等候 [新增 ASP.NET Core Web 應用程式] 對話方塊出現。

  • 選取 [.NET Core]、[ASP.NET Core 2.2] 和 [Web 應用程式 (Model-View-Controller)] 範本。

  • 確認 [驗證] 已設為 [No Authentication] (無驗證)

  • 選取確定

    New ASP.NET Core Project dialog

設定網站樣式

一些簡單的變更會設定網站的功能表、配置和首頁。

開啟 Views/Shared/_Layout.cshtml,然後進行下列變更:

  • 將每個出現的 "ContosoUniversity" 都變更為 "Contoso University"。 共有三個發生次數。

  • 為 [About]、[Students]、[Courses]、[Instructors] 及 [Departments] 新增功能表項目,並刪除 [Privacy]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 metapackage 中,因此您不需要參考該套件。

EF SQL Server 套件及其相依性 (Microsoft.EntityFrameworkCoreMicrosoft.EntityFrameworkCore.Relational) 提供了 EF 的執行階段支援。 您會在稍後的移轉教學課程中新增工具套件。

如需其他 Entity Framework Core 可用之資料庫提供者的資訊,請參閱資料庫提供者

建立資料模型

接下來您會為 Contoso 大學應用程式建立實體類別。 您會從下列三個實體開始。

Course-Enrollment-Student data model diagram

StudentEnrollment 實體之間存在一對多關聯性,CourseEnrollment 實體之間也存在一對多關聯性。 換句話說,一位學生可以註冊並參加任何數目的課程,而一個課程也可以有任何數目的學生註冊。

在下節中,您會為這些實體建立各自的類別。

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 預設會將名為 IDclassnameID 的屬性解譯為主索引鍵。

Enrollments 屬性為導覽屬性。 導覽屬性會保留與此實體相關的其他實體。 在這個案例中,Student entityEnrollments 屬性會保有與該 Student 實體相關的所有 Enrollment 實體。 換句話說,若資料庫中 Student 資料列有兩個相關的Enrollment 資料列 (包含該學生於其 StudentID 外部索引鍵資料行中主索引鍵值的資料列),該 Student 實體的 Enrollments 導覽屬性便會包含這兩個 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 屬性將為主索引鍵。此實體會使用 classnameID 模式,而非您在 Student 實體中所見到的自身 ID。 通常您會選擇一個模式,然後在您整個資料模型中使用此模式。 在這裡,此變化僅作為向您展示使用不同模式之用。 在稍後的教學課程中,您會了解使用沒有 classname 的識別碼可讓在資料模型中實作繼承變得更為簡單。

Grade 屬性為 enum。 問號之後的 Grade 類型宣告表示 Grade 屬性可為 Null。 為 Null 的成績不同於成績為零:Null 表示成績未知或尚未指派。

StudentID 屬性是外部索引鍵,對應的導覽屬性是 StudentEnrollment 實體與一個 Student 實體關聯,因此屬性僅能保有單一 Student 實體 (不像您先前看到的 Student.Enrollments 導覽屬性可保有多個 Enrollment 實體)。

CourseID 屬性是外部索引鍵,對應的導覽屬性是 Course。 一個 Enrollment 實體與一個 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 屬性為導覽屬性。 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 屬性名稱相同的資料表。 集合的屬性名稱通常都是複數 (Students 而非 Student),但許多開發人員會為了資料表名稱究竟是否該是複數型態而爭論。 在此系列教學課程中,您會藉由指定 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 Database Engine,旨在用於應用程式開發,而不是生產用途。 LocalDB 會依需求啟動,並以使用者模式執行,因此沒有複雜的組態。 LocalDB 預設會在 C:/Users/<user> 目錄中建立 .mdf 資料庫檔案。

使用測試資料將 DB 初始化

Entity Framework 會為您建立空白資料庫。 在本節中,您會撰寫一個方法,該方法會在資料庫建立之後呼叫,以將測試資料填入資料庫。

在此您將使用 EnsureCreated 方法來自動建立資料庫。 在稍後的教學課程中,您將會了解到如何使用 Code First 移轉變更資料庫結構描述,而非卸除並重新建立資料庫,來處理模型的變更。

在 [Data] 資料夾中,建立一個名為 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 方法來在應用程式啟動期間執行下列動作:

  • 從相依性插入容器中取得資料庫內容執行個體。
  • 呼叫種子方法,並將其傳遞給內容。
  • 種子方法完成時處理內容。
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 中的 Scaffolding 引擎來新增使用 EF 查詢和儲存資料的 MVC 控制器及檢視。

自動建立 CRUD 動作方法和檢視稱為 Scaffolding。 Scaffolding 與產生程式碼不同。Scaffold 程式碼是一個開始點,使得您可以修改它以符合您的需求,然而您通常不會去修改產生的程式碼。 當您需要自訂產生的程式碼時,您會使用部分類別,或者您會在事務變更時重新產生程式碼。

  • 以滑鼠右鍵按一下 [方案總管] 中的 [Controllers] 資料夾,然後選取 [新增] > [新增 Scaffold 項目]
  • 在 [新增 Scaffold] 對話方塊中:
    • 選取 [使用 Entity Framework 執行檢視的 MVC 控制器]
    • 按一下新增。 [新增使用 Entity Framework 執行檢視的 MVC 控制器] 對話方塊隨即出現:Scaffold Student
    • 在 [模型類別] 中,選取 [Student]
    • 在 [資料內容類別] 中,選取 [SchoolContext]
    • 接受預設的 StudentsController 作為名稱。
    • 按一下新增

Visual Studio Scaffolding 引擎會建立 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 物件總管 (SSOX) 來在 Visual Studio 中檢視資料庫。

關閉瀏覽器。

若 SSOX 視窗尚未開啟,請從 Visual Studio 中的 [檢視] 功能表選取它。

在 SSOX 中,按一下 [(localdb)\MSSQLLocalDB] > [Databases],然後按一下位於 appsettings.json 檔案中連接字串內資料庫名稱的項目。

展開 [資料表] 節點以查看資料庫中的資料表。

Tables in SSOX

以滑鼠右鍵按一下 Students 資料表,並按一下 [檢視資料] 查看建立的資料行及插入資料表中的資料列。

Student table in SSOX

.mdf.ldf 資料庫檔案位於 C:\Users<使用者名稱> 資料夾中。

因為您在應用程式啟動時執行的初始設定式方法中呼叫了 EnsureCreated,您現在可以對 Student 類別進行變更、刪除資料庫、重新執行應用程式,資料庫會自動重新建立以符合您所作出的變更。 例如,若您將一個 EmailAddress 屬性新增到 Student 類別,您便會在重新建立的資料表中看到新的 EmailAddress 資料行。

慣例

為了讓 Entity Framework 能夠建立一個完整資料庫,您所需要撰寫的程式碼非常少,多虧了慣例的使用及 Entity Framework 所做出的假設。

  • DbSet 屬性的名稱會用於資料表名稱。 針對 DbSet 屬性並未參考的實體,實體類別名稱會用於資料表名稱。
  • 實體屬性名稱會用於資料行名稱。
  • 命名為 ID 或 classnameID 的實體屬性,會辨識為主索引鍵屬性。
  • 如果屬性命名為 <導覽屬性名稱><主索引鍵屬性名稱>,系統就會將該屬性解譯為外部索引鍵屬性 (例如,若為 Student 導覽屬性則為 StudentID,因為 Student 實體的主索引鍵是 ID)。 外部索引鍵屬性也可以直接命名為 <主索引鍵屬性名稱> (例如 EnrollmentID,因為 Enrollment 實體的主索引鍵為EnrollmentID)。

慣例行為可以被覆寫。 例如,您可以明確指定資料表名稱,如稍早在本教學課程中您所見到的。 您可以設定資料行名稱以及將任何屬性設為主索引鍵或外部索引鍵,如同您在本系列稍後的教學課程中所見。

非同步程式碼

非同步程式設計是預設的 ASP.NET Core 和 EF Core 模式。

網頁伺服器的可用執行緒數量有限,而且在高負載情況下,可能會使用所有可用的執行緒。 發生此情況時,伺服器將無法處理新的要求,直到執行緒空出來。 使用同步程式碼,許多執行緒可能在實際上並未執行任何工作時受到占用,原因是在等候 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 關鍵字會使編譯器將方法分割為兩部分。 第一個部分會與非同步啟動的作業一同結束。 第二個部分則會放入作業完成時呼叫的回呼方法中。
  • ToListAsyncToList 擴充方法的非同步版本。

當您在撰寫使用 Entity Framework 的非同步程式碼時,請注意下列事項:

  • 只有讓查詢或命令傳送至資料庫的陳述式,會以非同步方式執行。 其中包含,例如 ToListAsyncSingleOrDefaultAsync,以及 SaveChangesAsync。 其中不包含,例如:僅變更 IQueryable 的陳述式,例如 var students = context.Students.Where(s => s.LastName == "Davolio")
  • EF 內容在執行緒中並不安全:不要嘗試執行多個平行作業。 當您呼叫任何 async EF 方法時,請一律使用 await 關鍵字。
  • 若您想要充分利用非同步程式碼帶來的效能優點,請確保任何您正在使用的程式庫 (例如分頁) 也使用了非同步 (若它們有呼叫任何可能會傳送查詢到資料庫的 Entity Framework 方法的話)。

如需在 .NET 中非同步程式設計的詳細資訊,請參閱非同步總覽

下一步

若要了解如何執行基本的 CRUD (建立、讀取、更新、刪除) 作業,請前往下一個教學課程。