作者: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 教程中包含但此 MVC 教程中没有的某些内容:
- 使用 Select 方法加载相关数据
- EF 最佳做法。
Contoso University 示例 Web 应用演示了如何使用 Entity Framework (EF) Core 和 Visual Studio 创建 ASP.NET Core MVC Web 应用。
该示例应用是一个虚构的 Contoso University 的网站。 其中包括学生录取、课程创建和讲师分配等功能。 这是介绍如何构建 Contoso University 示例应用系列教程中的第一部分。
Prerequisites
- 如果不熟悉 ASP.NET Core MVC,则在开始前,请浏览 ASP.NET Core MVC 入门教程。
- 带有 ASP.NET 和 Web 开发工作负载的 Visual Studio 2022。
- .NET 6 SDK
本教程尚未针对 .NET 6 或更高版本中的 ASP.NET Core 进行更新。 如果在 .NET 6 或更高版本中创建面向 ASP.NET Core 的项目,则本教程的说明将无法正常工作。 例如,.NET 6 或更高版本 Web 模板中的 ASP.NET Core 使用 最小的托管模型,该模型统 Startup.cs 一并 Program.cs 放入单个 Program.cs 文件中。
在 .NET 6 中引入的另一项变化是 NRT (可为空引用类型)这一功能。 项目模板默认启用此功能。 如果 EF 认为某个属性在 .NET 6 中是必需的,而在 .NET 5 中可为空,则可能会出现问题。 例如,除非将 Enrollments 属性设为可空,或将 asp-validation-summary 帮助器标记从 ModelOnly 改为 All,否则“创建学生”页面将静默失败。
对于本教程,建议安装并使用 .NET 5 SDK。 在本教程更新之前,请参阅 RazorASP.NET Core 中使用 Entity Framework Core 的页面 - 教程 1/8,了解如何在 .NET 6 或更高版本中将 Entity Framework 与 ASP.NET Core 一起使用。
数据库引擎
Visual Studio 指令使用 SQL Server LocalDB,它是只在 Windows 上运行的一种 SQL Server Express 版本。
解决问题和进行故障排除
如果遇到无法解决的问题,可以通过与 已完成的项目对比代码来查找解决方案。 常见错误以及对应的解决方案,请参阅 最新教程中的故障排除。 如果没有找到遇到的问题的解决方案,可以将问题发布到StackOverflow.com 的 ASP.NET Core 或 EF Core 版块。
Tip
这是一系列一共有十个教程,其中每个都是在前面教程已完成的基础上继续。 请考虑在完成每一个教程后保存项目的副本。 这样一来,如果你遇到问题,就可以从上一个教程重新开始,而不必回到整个系列的开头重来。
Contoso University Web 应用
这些教程中所构建的应用是一个基本的大学网站。
用户可以查看和更新学生、课程和讲师信息。 以下是该应用中的几个屏幕:
创建 Web 应用
- 启动 Visual Studio 并选择“创建新项目”。
- 在“新建项目”对话框中,选择“ASP.NET Core Web 应用程序”“下一步”。
- 在“配置新项目”对话框中,为“项目名称”输入 。 请务必使用此名称(含大写),确保在复制代码时与每个
namespace都相匹配。 - 选择 创建。
- 在“创建新的 ASP.NET Core Web 应用程序”对话框中,选择:
- 在下拉列表中选择.NET Core和ASP.NET Core 5.0。
- ASP.NET Core Web 应用程序(模型-视图-控制器)。
-
创建

设置网站样式
一些基本更改设置网站菜单、布局和主页。
打开 Views/Shared/_Layout.cshtml 并进行下列更改:
- 将每次出现的
ContosoUniversity更改为Contoso University。 共有三处出现。 - 添加“关于”、“学生”、“课程”、“讲师”和“院系”的菜单项,并删除“”菜单项。
前面的更改在以下代码中突出显示:
<!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/cu-final">See project source code »</a></p>
</div>
</div>
按 Ctrl+F5 运行项目,或从菜单中选择“调试 > 不调试启动”。 主页上会显示本教程中创建页面的选项卡。
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实体之间存在一对多关系。 一门课程中可以包含任意数量的学生。
以下部分将为这几个实体中的每一个实体创建一个类。
学生实体
在 Models 文件夹中,使用以下代码创建 类:
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 属性是导航属性。 导航属性中包含与此实体相关的其他实体。
Enrollments 实体的 Student 属性:
- 包含与该
Enrollment实体相关的所有Student实体。 - 如果数据库中的特定
Student行具有两个相关的Enrollment行:-
Student实体的Enrollments导航属性包含这两个Enrollment实体。
-
Enrollment 行在 StudentID 外键 (FK) 列中包含该学生的 PK 值。
如果导航属性可以包含多个实体:
- 类型必须为列表,如
ICollection<T>、List<T>或HashSet<T>。 - 可以添加、删除和更新实体。
多对多和一对多导航关系可包含多个实体。 使用 ICollection<T> 时,EF 会默认创建 HashSet<T> 集合。
注册实体
在 Models 文件夹中,使用以下代码创建 类:
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 表示级别未知或尚未分配。
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。
课程实体
在 Models 文件夹中,使用以下代码创建 类:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int CourseID { get; set; }
public string Title { get; set; }
public int Credits { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
Enrollments 属性是导航属性。
Course 实体可与任意数量的 Enrollment 实体相关。
DatabaseGenerated 属性将在后面的教程部分中介绍。 该属性允许为课程输入 PK,而不是让数据库生成 PK。
创建数据库上下文
DbContext 数据库上下文类是为给定数据模型协调 EF 功能的主类。 此类由 Microsoft.EntityFrameworkCore.DbContext 类派生而来。
DbContext 派生类指定数据模型中包含哪些实体。 某些 EF 行为可自定义。 在此项目中,该类名为 SchoolContext。
在项目文件夹中,创建名为的文件夹 Data。
在 Data 文件夹中,使用以下代码创建一个 类:
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
public DbSet<Course> Courses { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
}
}
前面的代码为每个实体集创建 DbSet 属性。 在 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 数据库引擎,专门针对应用开发,而非生产使用。 LocalDB 可按需启动并在用户模式下运行,因此无需复杂配置。 默认情况下,LocalDB 会在 目录中创建 .mdf 数据库文件。
使用测试数据初始化数据库
EF 将创建空数据库。 在本部分中,你将添加一个方法,该方法将在数据库创建后调用,以向数据库填充测试数据。
EnsureCreated 方法用于自动创建数据库。 在后面的教程中,你将了解如何处理模型更改,方法是使用 Code First 迁移来更改数据库架构,而不是删除和重新创建数据库。
在 Data 文件夹中,使用以下代码创建一个名为 的新类:
using ContosoUniversity.Models;
using System;
using System.Linq;
namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
context.Database.EnsureCreated();
// Look for any students.
if (context.Students.Any())
{
return; // DB has been seeded
}
var students = new Student[]
{
new Student{FirstMidName="Carson",LastName="Alexander",EnrollmentDate=DateTime.Parse("2005-09-01")},
new Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Parse("2002-09-01")},
new Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse("2003-09-01")},
new Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Parse("2002-09-01")},
new Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2002-09-01")},
new Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Parse("2001-09-01")},
new Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse("2003-09-01")},
new Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Parse("2005-09-01")}
};
foreach (Student s in students)
{
context.Students.Add(s);
}
context.SaveChanges();
var courses = new Course[]
{
new Course{CourseID=1050,Title="Chemistry",Credits=3},
new Course{CourseID=4022,Title="Microeconomics",Credits=3},
new Course{CourseID=4041,Title="Macroeconomics",Credits=3},
new Course{CourseID=1045,Title="Calculus",Credits=4},
new Course{CourseID=3141,Title="Trigonometry",Credits=4},
new Course{CourseID=2021,Title="Composition",Credits=3},
new Course{CourseID=2042,Title="Literature",Credits=4}
};
foreach (Course c in courses)
{
context.Courses.Add(c);
}
context.SaveChanges();
var enrollments = new Enrollment[]
{
new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
new Enrollment{StudentID=3,CourseID=1050},
new Enrollment{StudentID=4,CourseID=1050},
new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
new Enrollment{StudentID=6,CourseID=1045},
new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
};
foreach (Enrollment e in enrollments)
{
context.Enrollments.Add(e);
}
context.SaveChanges();
}
}
}
前面的代码检查数据库是否存在:
- 如果找不到数据库;
- 它会被创建并载入测试数据。 代码中使用数组存放测试数据而不是使用
List<T>集合是为了优化性能。
- 它会被创建并载入测试数据。 代码中使用数组存放测试数据而不是使用
- 如果找到数据库,则不执行任何操作。
使用以下代码更新 Program.cs:
using ContosoUniversity.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
namespace ContosoUniversity
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
CreateDbIfNotExists(host);
host.Run();
}
private static void CreateDbIfNotExists(IHost host)
{
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<SchoolContext>();
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB.");
}
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
Program.cs 在应用启动时执行以下操作:
- 从依赖注入容器中获取数据库上下文实例。
- 调用
DbInitializer.Initialize方法。 - 在
Initialize方法完成后释放上下文,如以下代码所示:
public static void Main(string[] args)
{
var host = CreateWebHostBuilder(args).Build();
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<SchoolContext>();
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred while seeding the database.");
}
}
host.Run();
}
第一次运行该应用时,会创建数据库并使用测试数据加载该数据库。 每当数据模型发生更改时:
- 删除数据库。
- 更新种子设定方法,并使用新数据库重新开始。
在后面的教程中,在更改数据模型时会修改数据库,而不会删除和重新创建数据库。 数据模型发生更改时不会丢失任何数据。
创建控制器和视图
使用 Visual Studio 中的脚手架引擎添加一个 MVC 控制器和视图,以便使用 EF 查询和保存数据。
CRUD 操作方法和视图的自动创建被称为基架。
- 在 解决方案资源管理器 中,右键单击
Controllers文件夹,然后选择 添加 新建基架项。 - 在“添加基架”对话框中:
- 选择 带视图、使用 Entity Framework 的 MVC 控制器。
- 单击 添加。 出现 添加使用 Entity Framework 的带视图 MVC 控制器 对话框:

- 在模型类中,选择Student。
- 在“数据上下文类”中选择“SchoolContext” 。
- 使用 StudentsController 作为默认名称。
- 单击 添加。
Visual Studio 基架引擎创建 StudentsController.cs 文件和一组对应于控制器的视图(*.cshtml 文件)。
注意控制器将 SchoolContext 作为构造函数参数。
namespace ContosoUniversity.Controllers
{
public class StudentsController : Controller
{
private readonly SchoolContext _context;
public StudentsController(SchoolContext context)
{
_context = context;
}
ASP.NET Core 依赖关系注入负责将 SchoolContext 实例传递到控制器。 你在 Startup 类中对其进行配置。
控制器包含 Index 操作方法,用于显示数据库中的所有学生。 该方法通过读取数据库上下文实例的 Students 属性,从 Students 实体集中获取学生列表:
public async Task<IActionResult> Index()
{
return View(await _context.Students.ToListAsync());
}
本教程后面部分将介绍此代码中的异步编程元素。
Views/Students/Index.cshtml 视图以表格形式显示此列表:
@model IEnumerable<ContosoUniversity.Models.Student>
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.LastName)
</th>
<th>
@Html.DisplayNameFor(model => model.FirstMidName)
</th>
<th>
@Html.DisplayNameFor(model => model.EnrollmentDate)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
按 Ctrl+F5 运行项目,或从菜单中选择“调试 > 不调试启动”。
单击“学生”选项卡,以查看由 DbInitializer.Initialize 方法插入的测试数据。 根据浏览器窗口的宽窄程度,你会在页面顶部看到 Students 选项卡链接,或者需要点击右上角的导航图标才能看到该链接。
查看数据库
启动应用时,DbInitializer.Initialize 方法会调用 EnsureCreated。 EF 发现不存在数据库:
- 因此,它创建了一个数据库。
-
Initialize方法代码用数据填充数据库。
在 Visual Studio 中使用“SQL Server 对象资源管理器”(SSOX) 查看数据库:
- 从 Visual Studio 中的“视图”菜单选择“SQL Server 对象资源管理器”。
- 在 SSOX 中,选择 “(localdb)\MSSQLLocalDB > 数据库”。
- 选择
ContosoUniversity1,即appsettings.json文件中的连接字符串里用于指定数据库名称的条目。 - 展开 Tables 节点,即可查看数据库中的表。
右键单击“Student”表,然后单击“查看数据”以查看表中的数据。
*.mdf 和 *.ldf 数据库文件位于“C:\Users\<username>”文件夹中。
由于 EnsureCreated 是在应用启动时运行的初始化方法中调用的,因此可以执行以下操作:
- 对
Student类进行更改。 - 删除数据库。
- 停止,然后启动应用。 将自动重新创建数据库以匹配此更改。
例如,如果将属性 EmailAddress 添加到 Student 类, EmailAddress 则会在重新创建的表中创建新列。 视图不会显示新的 EmailAddress 属性。
Conventions
由于使用了 EF 使用的约定,因此为使 EF 创建完整数据库而编写的代码量是最小的:
-
DbSet属性的名称用作表名。 如果实体未被DbSet属性引用,实体类名称用作表名称。 - 使用实体属性名作为列名。
- 命名为
ID或classnameID的实体属性识别为 PK 属性。 - 如果将属性命名为
<导航属性名称><PK 属性名称>,则该属性解释为 FK 属性。 例如,对于Student导航属性,使用StudentID,因为Student实体的 PK 是ID。 还可以将 FK 属性命名为<主键属性名称>。 例如EnrollmentID,因为Enrollment实体的 PK 为EnrollmentID。
默认行为可以被覆盖。 例如,可以显式指定表名,如本教程中前面部分所示。 列名称和任何属性都可以设置为 PK 或 FK。
异步代码
异步编程是 ASP.NET Core 和 EF Core 的默认模式。
Web 服务器的可用线程是有限的,而在高负载情况下的可能所有线程都被占用。 当发生这种情况的时候,服务器就无法处理新请求,直到线程被释放。 使用同步代码时,可能会出现多个线程被占用但不能执行任何操作的情况,因为它们正在等待 I/O 完成。 使用异步代码时,当进程正在等待 I/O 完成,服务器可以将其线程释放用于处理其他请求。 因此,异步代码使得服务器更有效地使用资源,并且该服务器可以无延迟地处理更多流量。
异步代码在运行时,会引入的少量开销,在低流量时对性能的影响可以忽略不计,但在针对高流量情况下潜在的性能提升是可观的。
在下面的代码中,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 上下文是线程不安全的: 请勿尝试并行执行多个操作。 当调用异步 EF 方法时,始终使用
await关键字。 - 若要利用异步代码的性能优势,请确保所使用的任何库包在它们调用导致发送至数据库的查询的任何 EF 方法时也使用异步。
有关在 .NET 异步编程的详细信息,请参阅 异步概述。
限制提取的实体数
若要了解如何限制从查询返回的实体数,请参阅性能注意事项。
Entity Framework Core 的 SQL 日志记录
日志配置通常由 Logging 文件的 appsettings.{Environment}.json 部分提供。 若要记录 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 输出窗口中。
有关详细信息,请参阅以下资源:
请继续阅读下一篇教程,了解如何执行基本的 CRUD(创建、读取、更新、删除)操作。
本教程介绍具有控制器和视图的 ASP.NET Core MVC 和 Entity Framework Core。 Razor Pages 是一种备用编程模型。 对于新的开发,我们建议在具有控制器和视图的 MVC 上使用 Razor Pages。 请参阅本教程的 Razor Pages 版本。 每个教程涵盖其他教程没有的一些资料:
此 MVC 教程中包含但 Razor Pages 教程中没有的某些内容:
- 在数据模型中实现继承
- 执行原始 SQL 查询
- 使用动态 LINQ 简化代码
Razor Pages 教程中包含但此 MVC 教程中没有的某些内容:
- 使用 Select 方法加载相关数据
- EF 最佳做法。
Contoso University 示例 Web 应用程序演示如何使用 Entity Framework (EF) Core 2.2 和 Visual Studio 2019 创建 ASP.NET Core 2.2 MVC Web 应用程序。
本教程没有针对 ASP.NET Core 3.1 进行更新。 它已更新为 .NET 5 中的 ASP.NET Core。
该示例应用程序是一个虚构的 Contoso 大学的网站。 其中包括学生录取、课程创建和讲师分配等功能。 这是一系列教程中的第一个,这一系列教程主要展示了如何从零开始构建 Contoso 大学示例应用程序。
Prerequisites
- .NET Core SDK 2.2
-
Visual Studio 2019 包含以下工作负荷:
- ASP.NET 和 Web 开发工作负荷
- .NET Core 跨平台开发工作负荷
Troubleshooting
如果遇到无法解决的问题,可以通过与 已完成的项目对比代码来查找解决方案。 常见错误以及对应的解决方案,请参阅 最新教程中的故障排除。 如果没有找到遇到的问题的解决方案,可以将问题发布到StackOverflow.com 的 ASP.NET Core 或 EF Core 版块。
Tip
这是一系列一共有十个教程,其中每个都是在前面教程已完成的基础上继续。 请考虑在完成每一个教程后保存项目的副本。 这样一来,如果你遇到问题,就可以从上一个教程重新开始,而不必回到整个系列的开头重来。
Contoso University Web 应用
你将在这些教程中学习构建一个简单的大学网站的应用程序。
用户可以查看和更新学生、课程和讲师信息。 以下是一些你即将创建的页面。
创建 Web 应用
打开 Visual Studio。
在文件菜单中,选择新建项目。
从左窗格中依次选择“已安装”>“Visual C#”>“Web”。
选择“ASP.NET Core Web 应用程序”项目模板。
输入“ContosoUniversity”作为名称,然后单击“确定” 。
等待“新建 ASP.NET Core Web 应用程序”对话框显示出来。
选择“.NET Core”、“ASP.NET Core 2.2”和“Web 应用程序(模型-视图-控制器)”模板 。
请确保 身份验证 设置为 不进行身份验证。
选择 “确定”
设置网站样式
一些简单的更改将设置网站菜单、布局和主页。
打开 Views/Shared/_Layout.cshtml 并进行下列更改:
将每个“ContosoUniversity”更改为“Contoso University”。 共有三处出现。
添加“关于”、“学生”、“课程”、“讲师”和“院系”的菜单项,并删除“”菜单项。
突出显示所作更改。
<!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 运行项目,或从菜单中选择“调试 > 不调试启动”。 你将看到主页,其中包含将在这些教程中创建的页面的选项卡。
关于 EF Core NuGet 包
若要为项目添加 EF Core 支持,请安装相应的数据提供程序。 本教程使用 SQL Server,相关驱动包Microsoft.EntityFrameworkCore.SqlServer。 此包包含在 Microsoft.AspNetCore.App 元包中,因此无需引用该包。
EF SQL Server 包和其依赖项(Microsoft.EntityFrameworkCore 和 Microsoft.EntityFrameworkCore.Relational)一起提供 EF 的运行时支持。 你将在之后的 迁移 教程中学习添加工具包。
有关其他可用于 EF Core 的数据库驱动的信息,请参阅 数据库驱动。
创建数据模型
接下来你将创建 Contoso 大学应用程序的实体类。 你将从以下三个实体类开始。
Student 和 Enrollment 实体之间是一对多关系,Course 和 Enrollment 实体之间也存在一对多关系。 换而言之,一名学生可以修读任意数量的课程, 并且某一课程可以被任意数量的学生修读。
接下来,你将创建与这些实体对应的类。
学生实体
在 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 属性将成为与此类对应的数据库表中的主键列。 默认情况下,EF 将会将名为 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> 集合。
注册实体
在 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 模式而不是如 ID 实体那样直接使用 Student。 通常情况下,你会选择一种模式,并在整个数据模型中始终使用它。 这里,这种变化表明你可以使用任一种模式。 在后面的教程中,你会看到仅使用 ID 而不使用 classname,如何让在数据模型中实现继承变得更容易。
Grade 属性为 enum。 在 Grade 类型声明后面的 `?` 表示 Grade 属性可为 null。 值为 null 的评分与 0 分是不同的——null 表示该评分未知,或尚未被赋值。
StudentID 属性是一个外键,Student 是与其且对应的导航属性。 一个 Enrollment 实体与一个 Student 实体相关联,因此该属性只能保存单个 Student 实体(不同于前面看到的 Student.Enrollments 导航属性,后者可以保存多个 Enrollment 实体)。
CourseID 属性是一个外键,Course 是与其且对应的导航属性。
Enrollment 实体与一个 Course 实体相关联。
如果某个属性命名为 <navigation property name><primary key property name>,Entity Framework 就会将其识别为外键属性(例如,对于 Student 导航属性,由于 Student 实体的主键是 ID,因此 StudentID 会被识别为外键属性)。 此外,外键属性也可以直接命名为 <primary key property name>(例如,CourseID,因为 Course 实体的主键是 CourseID)。
课程实体
在 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 数据库引擎的轻量级版本,用于应用程序开发,不在生产环境中使用。 LocalDB 可按需启动并在用户模式下运行,因此无需复杂配置。 默认情况下, LocalDB 在 目录下创建 C:/Users/<user> 数据库文件。
使用测试数据初始化数据库
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 方法,使得在应用程序启动时能执行以下操作:
- 从依赖注入容器中获取数据库上下文实例。
- 调用 seed 方法,将上下文传递给它。
- Seed 方法完成此操作时释放上下文。
using ContosoUniversity.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
namespace ContosoUniversity
{
public class Program
{
public static void Main(string[] args)
{
var host = CreateHostBuilder(args).Build();
CreateDbIfNotExists(host);
host.Run();
}
private static void CreateDbIfNotExists(IHost host)
{
using (var scope = host.Services.CreateScope())
{
var services = scope.ServiceProvider;
try
{
var context = services.GetRequiredService<SchoolContext>();
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB.");
}
}
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
}
首次运行该应用程序时,将创建数据库并使用测试数据填充该数据库。 每次更改数据模型时:
- 删除数据库。
- 更新 Seed 方法,并以相同的方式用新数据库重新开始。
在后续教程中,你会看到如何在数据模型发生变化时修改数据库,而无需删除并重新创建数据库。
创建控制器和视图
在本节,将使用 Visual Studio 中的基架引擎添加一个 MVC 控制器,以及使用 EF 来查询和保存数据的视图。
自动创建 CRUD 操作方法和视图的过程称为脚手架。 脚手架与代码生成不同,因为脚手架生成的代码只是一个起点,你可以根据自己的需求对其进行修改,而生成的代码通常不会被修改。 当你需要自定义生成代码时,可使用一部分类或需求发生变化时重新生成代码。
- 右键单击 解决方案资源管理器 中的 Controllers 文件夹,然后选择 添加新建基架项。
- 在“添加基架”对话框中:
- 选择 带视图、使用 Entity Framework 的 MVC 控制器。
- 单击 添加。 出现 添加使用 Entity Framework 的带视图 MVC 控制器 对话框:

- 在 模型类 选择 Student。
- 在“数据上下文类”中选择 SchoolContext 。
- 使用 StudentsController 作为默认名称。
- 单击 添加。
Visual Studio 基架引擎创建 StudentsController.cs 文件和一组对应于控制器的视图(.cshtml 文件)。
注意控制器将 SchoolContext 作为构造函数参数。
namespace ContosoUniversity.Controllers
{
public class StudentsController : Controller
{
private readonly SchoolContext _context;
public StudentsController(SchoolContext context)
{
_context = context;
}
ASP.NET Core 依赖关系注入负责将 SchoolContext 实例传递到控制器。 这是在 Startup.cs 文件中配置的。
控制器包含 Index 操作方法,用于显示数据库中的所有学生。 该方法通过读取数据库上下文实例的 Students 属性,从 Students 实体集中获取学生列表:
public async Task<IActionResult> Index()
{
return View(await _context.Students.ToListAsync());
}
本教程后面部分将介绍此代码中的异步编程元素。
Views/Students/Index.cshtml 视图在表格中显示此列表:
@model IEnumerable<ContosoUniversity.Models.Student>
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
@Html.DisplayNameFor(model => model.LastName)
</th>
<th>
@Html.DisplayNameFor(model => model.FirstMidName)
</th>
<th>
@Html.DisplayNameFor(model => model.EnrollmentDate)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.LastName)
</td>
<td>
@Html.DisplayFor(modelItem => item.FirstMidName)
</td>
<td>
@Html.DisplayFor(modelItem => item.EnrollmentDate)
</td>
<td>
<a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
<a asp-action="Details" asp-route-id="@item.ID">Details</a> |
<a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
</td>
</tr>
}
</tbody>
</table>
按 Ctrl+F5 运行项目,或从菜单中选择“调试 > 不调试启动”。
单击 Students 选项卡以查看由 DbInitializer.Initialize 方法插入的测试数据。 根据浏览器窗口的宽窄程度,你会在页面顶部看到 Students 选项卡链接,或者需要点击右上角的导航图标才能看到该链接。
查看数据库
当你启动了应用程序,DbInitializer.Initialize 方法调用 EnsureCreated。 EF 没有检测到相关数据库,因此自己创建了一个,接着 Initialize 方法的其余代码向数据库中填充数据。 你可以使用 Visual Studio 中的 SQL Server 对象资源管理器 (SSOX) 查看数据库。
关闭浏览器。
如果 SSOX 窗口尚未打开,请从Visual Studio 中的视图 菜单中选择。
在 SSOX 中,单击 (localdb)\MSSQLLocalDB > 数据库,然后单击 文件的连接字符串中数据库名称对应的条目。
展开 Tables 节点,即可查看数据库中的表。
右键单击 Student 表,然后单击 查看数据,即可查看已创建的列和已插入到表的行。
.mdf 和 .ldf 数据库文件位于“C:\Users\username”文件夹中。
因为调用 EnsureCreated 的初始化方法在启动应用程序时才运行,所以在这之前你可以更改 Student 类、 删除数据库、 再运行一次应用程序,这时候数据库将自动重新创建,以匹配所做的更改。 例如,如果向 EmailAddress 类添加 Student 属性,重新的创建表中会有 EmailAddress 列。
Conventions
由于 Entity Framework 有一定的约束条件,你只需要按规则编写很少的代码就能够创建一个完整的数据库。
-
DbSet属性的名称用作表名。 如果实体未被DbSet属性引用,实体类名称用作表名称。 - 使用实体属性名作为列名。
- 以 ID 或 classnameID 命名的实体属性被视为主键属性。
- 如果某个属性被命名为 <导航属性名称><主键属性名称>,则它会被解释为外键属性(例如,对于
Student导航属性,属性名为StudentID,因为Student实体的主键是 )。 外键属性也可以直接命名为 <主键属性名称>(例如,EnrollmentID,因为Enrollment实体的主键是 )。
默认行为可以被覆盖。 例如,你可以明确指定表名,正如你在本教程前面看到的那样。 你可以设置列名,并将任何属性设置为主键或外键,正如你将在本系列的后续教程中看到的那样。
异步代码
异步编程是 ASP.NET Core 和 EF Core 的默认模式。
Web 服务器的可用线程是有限的,而在高负载情况下的可能所有线程都被占用。 当发生这种情况的时候,服务器就无法处理新请求,直到线程被释放。 使用同步代码时,可能会出现多个线程被占用但不能执行任何操作的情况,因为它们正在等待 I/O 完成。 使用异步代码时,当进程正在等待 I/O 完成,服务器可以将其线程释放用于处理其他请求。 因此,异步代码使得服务器更有效地使用资源,并且该服务器可以无延迟地处理更多流量。
异步代码在运行时,会引入的少量开销,在低流量时对性能的影响可以忽略不计,但在针对高流量情况下潜在的性能提升是可观的。
在以下代码中,async 关键字、Task<T> 返回值、await 关键字和 ToListAsync 方法让代码异步执行。
public async Task<IActionResult> Index()
{
return View(await _context.Students.ToListAsync());
}
-
async关键字告诉编译器为方法主体中的某些部分生成回调,并自动创建要返回的Task<IActionResult>对象。 - 返回类型
Task<IActionResult>表示一项正在进行的操作,其结果类型为IActionResult。 -
await关键字会使得编译器将方法拆分为两个部分。 第一部分以异步启动的操作结束。 第二部分被放入一个在操作完成时调用的回调方法中。 -
ToListAsync是ToList方法的的异步扩展版本。
使用 Entity Framework 编写异步代码时的一些注意事项:
- 只有导致查询或发送数据库命令的语句才能以异步方式执行。 包括
ToListAsync,SingleOrDefaultAsync,和SaveChangesAsync。 例如,它不包括仅更改IQueryable的语句,如var students = context.Students.Where(s => s.LastName == "Davolio")。 - EF 上下文是线程不安全的: 请勿尝试并行执行多个操作。 当调用异步 EF 方法时,始终使用
await关键字。 - 如果你想利用异步代码带来的性能优势,请确保你使用的任何库包(例如用于分页的库包)在调用 Entity Framework 中任何会导致向数据库发送查询的方法时,也采用异步方式。
有关在 .NET 异步编程的详细信息,请参阅 异步概述。
后续步骤
请继续阅读下一篇教程,了解如何执行基本的 CRUD(创建、读取、更新、删除)操作。