教學課程:開始在 ASP.NET MVC Web 應用程式中使用 EF Core
作者:Tom Dykstra 和 Rick 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 MVC,請先前往開始使用 ASP.NET Core MVC 教學課程系列,再開始進行本教學課程。
- Visual Studio 2022 和 ASP.NET 與 Web 開發工作負載。
- .NET 6.0 SDK
此教學課程尚未升級至 ASP.NET Core 6 或更新版本。 如果您建立以 ASP.NET Core 6 或更新版本為目標的專案,本教學課程的指示將無法正確運作。 例如,ASP.NET Core 6 和更新版本網頁範本會使用最小裝載模型,這會將 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 Core 或 EF Core 的問題。
提示
這是 10 個教學的系列課程,當中的每一個課程都是建置於先前教學課程的成果上。 成功完成每一個教學課程後,請儲存專案的複本。 如果您遇到問題,您可以從上一個教學課程來重新開始,而不需從系列的一開始從頭來過。
Contoso 大學 Web 應用程式
在教學課程中建立的應用程式,是一個基本的大學網站。
使用者可以檢視和更新學生、課程和教師資訊。 以下是應用程式中的一些畫面:
建立 Web 應用程式
- 啟動 Visual Studio 並選取 [建立新專案]。
- 在 [建立新專案] 對話方塊中,選取 [ASP.NET Core Web 應用程式]>[下一步]。
- 在 [設定新專案] 對話方塊中,輸入
ContosoUniversity
作為 [專案名稱]。 使用一模一樣的名稱非常重要 (包括大小寫),用以確保在您複製和貼上程式碼時每個namespace
均相符。 - 選取建立。
- 在 [建立新的 ASP.NET Core Web 應用程式] 對話方塊中,選取:
- 下拉式清單中的 .NET Core 和 ASP.NET Core 5.0。
- ASP.NET Core Web 應用程式 (Model-View-Controller)。
- 建立
設定網站樣式
幾項基本變更就能設定網站選單、版面配置和 home 網頁。
開啟 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">
© 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 »</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 »</a></p>
</div>
</div>
按 CTRL+F5 來執行專案,或從功能表選擇 [偵錯] > [啟動但不偵錯]。 home網頁對於本教學課程中所建立的分頁會以標籤顯示。
EF Core NuGet 封裝
本教學課程使用 SQL Server,其提供者套件為 Microsoft.EntityFrameworkCore.SqlServer。
EF SQL Server 封裝及其相依性 (Microsoft.EntityFrameworkCore
及 Microsoft.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 可用的資料庫提供者資訊,請參閱資料庫提供者。
建立資料模型
此應用程式會建立下列實體類別:
上述實體具有下列關聯性:
Student
和Enrollment
實體之間具有一對多關聯性。 一位學生可註冊任何數目的課程。Course
和Enrollment
實體之間具有一對多關聯性。 一堂課程可以讓任意數目的學生註冊。
在下列一節中,將為這些實體建立各自的類別。
Student 實體
在 [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 可以命名為 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 實體
在 [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
模式,而非本身的 ID
。 Student
實體會使用 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 實體
在 [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 控制器] 對話方塊隨即出現:
- 在 [模型類別] 中,選取 [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
索引標籤連結,或是按一下位於右上角的導覽圖示來查看連結。
檢視資料庫
應用程式啟動時,DbInitializer.Initialize
方法會呼叫 EnsureCreated
。 EF 看到沒有資料庫:
- 其便會建立資料庫。
Initialize
方法程式碼會以資料填入資料庫。
使用 [SQL Server 物件總管] (SSOX) 以在 Visual Studio 中檢視資料庫:
- 從 Visual Studio 中的 [檢視] 功能表選取 [SQL Server 物件總管]。
- 在 SSOX 中,選取 [(localdb)\MSSQLLocalDB] > [資料庫]。
- 選取
ContosoUniversity1
,這是appsettings.json
檔案中連接字串內資料庫名稱的項目。 - 展開 [資料表] 節點以查看資料庫中的資料表。
以滑鼠右鍵按一下 [Student] 資料表,然後按一下 [檢視資料] 以查看資料表中的資料。
*.mdf
和 *.ldf
資料庫檔案皆位於 C:\Users\<使用者名稱> 資料夾中。
因為應用程式啟動時執行的初始化運算式方法會呼叫 EnsureCreated
,所以您可以:
- 對
Student
類別進行變更。 - 刪除資料庫。
- 停止,然後啟動應用程式。 資料庫會自動重新建立,以符合變更。
例如,若將 EmailAddress
屬性新增到 Student
類別,重新建立的資料表中應會出現新的 EmailAddress
資料行。 檢視不會顯示新的 EmailAddress
屬性。
慣例
為了讓 EF 建立完整資料庫所需撰寫的程式碼數量非常少,這是因為 EF 使用慣例的緣故:
DbSet
屬性的名稱會用於資料表名稱。 針對DbSet
屬性並未參考的實體,實體類別名稱會用於資料表名稱。- 實體屬性名稱會用於資料行名稱。
- 名為
ID
或classnameID
的實體屬性會辨識為 PK 屬性。 - 如果屬性命名為
<
導覽屬性名稱><
PK 屬性名稱>
,則該屬性將解譯為 PK 屬性。 例如,Student
導覽屬性的StudentID
,因為Student
實體的 PK 是ID
。 FK 屬性也可以命名為<
主索引鍵屬性名稱>
。 例如EnrollmentID
,因為Enrollment
實體的 PK 是EnrollmentID
。
慣例行為可以被覆寫。 例如,可以明確指定資料表名稱,如稍早在本教學課程中所示。 資料行名稱和任何屬性都可以設定為 PK 或 FK。
非同步程式碼
非同步程式設計是預設的 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
關鍵字會使編譯器將方法分割為兩部分。 第一個部分會與非同步啟動的作業一同結束。 第二個部分則會放入作業完成時呼叫的回呼方法中。ToListAsync
是ToList
擴充方法的非同步版本。
若要撰寫使用 EF 的非同步程式碼,請注意下列事項:
- 只有讓查詢或命令傳送至資料庫的陳述式,會以非同步方式執行。 其中包含,例如
ToListAsync
、SingleOrDefaultAsync
,以及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 大學範例應用程式教學課程系列中的第一頁。
必要條件
- .NET Core SDK 2.2
- Visual Studio 2019 和下列工作負載:
- ASP.NET 與網頁程式開發工作負載
- .NET Core 跨平台開發工作負載
疑難排解
如果您遭遇無法解決的問題,將您的程式碼與已完成的專案作比較,通常可以找到解答。 如需常見錯誤和如何解決這些問題的清單,請參閱 本系列最後一個教學課程中的疑難排解一節。 如果您在那裡找不到所需的資訊,可以在 StackOverflow.com 上張貼關於 ASP.NET Core 或 EF Core 的問題。
提示
這是 10 個教學的系列課程,當中的每一個課程都是建置於先前教學課程的成果上。 成功完成每一個教學課程後,請儲存專案的複本。 如果您遇到問題,您可以從上一個教學課程來重新開始,而不需從系列的一開始從頭來過。
Contoso 大學 Web 應用程式
您在這些教學課程中會建置的應用程式為一個簡單的大學網站。
使用者可以檢視和更新學生、課程和教師資訊。 以下是您會建立的幾個畫面。
建立 Web 應用程式
開啟 Visual Studio。
從 [檔案] 功能表選取[新增] > [專案]。
從左側窗格中,選取 [已安裝] > [Visual C#] > [Web]。
選取 [ASP.NET Core Web 應用程式] 專案範本。
輸入 ContosoUniversity 作為名稱,然後按一下 [確定]。
等候 [新增 ASP.NET Core Web 應用程式] 對話方塊出現。
選取 [.NET Core]、[ASP.NET Core 2.2] 和 [Web 應用程式 (Model-View-Controller)] 範本。
確認 [驗證] 已設為 [No Authentication] (無驗證)。
選取確定
設定網站樣式
一些簡易的變更會設定網站選單、版面配置和 home 分頁。
開啟 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">
© 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 »</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 »</a></p>
</div>
</div>
按 CTRL+F5 來執行專案,或從功能表選擇 [偵錯] > [啟動但不偵錯]。 您會看到 home 分頁具有標籤,可供索引您在這些教學課程中即將建立之分頁。
關於 EF Core NuGet 封裝
若要將 EF Core 支援新增至專案,請安裝您欲使用資料庫的提供者。 本教學課程使用 SQL Server,其提供者套件為 Microsoft.EntityFrameworkCore.SqlServer。 此套件包含在 Microsoft.AspNetCore.App metapackage 中,因此您不需要參考該套件。
EF SQL Server 套件及其相依性 (Microsoft.EntityFrameworkCore
及 Microsoft.EntityFrameworkCore.Relational
) 提供了 EF 的執行階段支援。 您會在稍後的移轉教學課程中新增工具套件。
如需其他 Entity Framework Core 可用之資料庫提供者的資訊,請參閱資料庫提供者。
建立資料模型
接下來您會為 Contoso 大學應用程式建立實體類別。 您會從下列三個實體開始。
在 Student
和 Enrollment
實體之間存在一對多關聯性,Course
與 Enrollment
實體之間也存在一對多關聯性。 換句話說,一位學生可以註冊並參加任何數目的課程,而一個課程也可以有任何數目的學生註冊。
在下節中,您會為這些實體建立各自的類別。
Student 實體
在 [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 entity
的 Enrollments
屬性會保有與該 Student
實體相關的所有 Enrollment
實體。 換句話說,若資料庫中 Student
資料列有兩個相關的Enrollment
資料列 (包含該學生於其 StudentID 外部索引鍵資料行中主索引鍵值的資料列),該 Student
實體的 Enrollments
導覽屬性便會包含這兩個 Enrollment
實體。
若導覽屬性可保有多個實體 (例如在多對多或一對多關聯性中的情況),其類型必須為一個清單,使得實體可以在該清單中新增、刪除或更新,例如 ICollection<T>
。 您可以指定 ICollection<T>
或如 List<T>
或 HashSet<T>
等類型。 若您指定了 ICollection<T>
,EF 會根據預設建立一個 HashSet<T>
集合。
Enrollment 實體
在 [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
屬性是外部索引鍵,對應的導覽屬性是 Student
。 Enrollment
實體與一個 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 實體
在 [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.Data
和 Microsoft.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 控制器] 對話方塊隨即出現:
- 在 [模型類別] 中,選取 [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
索引標籤連結,或是按一下位於右上角的導覽圖示來查看連結。
檢視資料庫
當您啟動應用程式時,DbInitializer.Initialize
方法會呼叫 EnsureCreated
。 EF 看到不存在任何資料庫,於是便建立了一個資料庫,接著 Initialize
方法程式碼的剩餘部分便會將資料填入資料庫。 您可以使用 [SQL Server 物件總管 (SSOX) 來在 Visual Studio 中檢視資料庫。
關閉瀏覽器。
若 SSOX 視窗尚未開啟,請從 Visual Studio 中的 [檢視] 功能表選取它。
在 SSOX 中,按一下 [(localdb)\MSSQLLocalDB] > [Databases],然後按一下位於 appsettings.json
檔案中連接字串內資料庫名稱的項目。
展開 [資料表] 節點以查看資料庫中的資料表。
以滑鼠右鍵按一下 Students 資料表,並按一下 [檢視資料] 查看建立的資料行及插入資料表中的資料列。
.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
關鍵字會使編譯器將方法分割為兩部分。 第一個部分會與非同步啟動的作業一同結束。 第二個部分則會放入作業完成時呼叫的回呼方法中。ToListAsync
是ToList
擴充方法的非同步版本。
當您在撰寫使用 Entity Framework 的非同步程式碼時,請注意下列事項:
- 只有讓查詢或命令傳送至資料庫的陳述式,會以非同步方式執行。 其中包含,例如
ToListAsync
、SingleOrDefaultAsync
,以及SaveChangesAsync
。 其中不包含,例如:僅變更IQueryable
的陳述式,例如var students = context.Students.Where(s => s.LastName == "Davolio")
。 - EF 內容在執行緒中並不安全:不要嘗試執行多個平行作業。 當您呼叫任何 async EF 方法時,請一律使用
await
關鍵字。 - 若您想要充分利用非同步程式碼帶來的效能優點,請確保任何您正在使用的程式庫 (例如分頁) 也使用了非同步 (若它們有呼叫任何可能會傳送查詢到資料庫的 Entity Framework 方法的話)。
如需在 .NET 中非同步程式設計的詳細資訊,請參閱非同步總覽。
下一步
若要了解如何執行基本的 CRUD (建立、讀取、更新、刪除) 作業,請前往下一個教學課程。