ASP.NET Core 中的 Razor 页面和 Entity Framework Core
Contoso University 示例 Web 应用演示了如何使用 Entity Framework (EF) Core 创建 ASP.NET Core Razor Pages 应用。
该示例应用是一个虚构的 Contoso University 的网站。 其中包括学生录取、课程创建和讲师分配等功能。 本页是介绍如何构建 Contoso University 示例应用系列教程中的第一部分。
系统必备
具有以下工作负载的 Visual Studio 2017 15.7.3 版或更高版本:
- ASP.NET 和 Web 开发
- .NET Core 跨平台开发
熟悉 Razor 页面。 新程序员在开始学习本系列之前,应先完成 Razor 页面入门。
疑难解答
如果遇到无法解决的问题,可以通过与 已完成的项目对比代码来查找解决方案。 获取帮助的一个好方法是将问题发布到适用于 ASP.NET Core 或 EF Core 的 StackOverflow.com。
Contoso University Web 应用
这些教程中所构建的应用是一个基本的大学网站。
用户可以查看和更新学生、课程和讲师信息。 以下是在本教程中创建的几个屏幕。
此网站的 UI 样式与内置模板生成的 UI 样式类似。 教程的重点是 EF Core 和 Razor 页面,而非 UI。
创建 ContosoUniversity Razor Pages Web 应用
- 从 Visual Studio“文件”菜单中,选择“新建”>“项目”。
- 创建新的 ASP.NET Core Web 应用程序。 将该项目命名为 ContosoUniversity 。 务必将该项目命名为 ContosoUniversity,以便复制/粘贴代码时命名空间相匹配。
- 在下拉列表中选择“ASP.NET Core 2.1”,然后选择“Web 应用程序”。
有关上述步骤的图像,请参阅创建 Razor Web 应用。 运行应用。
设置网站样式
设置网站菜单、布局和主页时需作少量更改。 进行以下更改以更新 Pages/Shared/_Layout.cshtml:
- 将文件中的"ContosoUniversity"更改为"Contoso University"。 需要更改三个地方。
- 添加菜单项 Students,Courses,Instructors,和 Department,并删除 Contact菜单项。
突出显示所作更改。 (所有标记均不显示。)
HTML
<!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" />
<link rel="stylesheet" href="~/css/site.css" />
</environment>
<environment exclude="Development">
<link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute" />
<link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
</environment>
</head>
<body>
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a asp-page="/Index" class="navbar-brand">Contoso University</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li><a asp-page="/Index">Home</a></li>
<li><a asp-page="/About">About</a></li>
<li><a asp-page="/Students/Index">Students</a></li>
<li><a asp-page="/Courses/Index">Courses</a></li>
<li><a asp-page="/Instructors/Index">Instructors</a></li>
<li><a asp-page="/Departments/Index">Departments</a></li>
</ul>
</div>
</div>
</nav>
<partial name="_CookieConsentPartial" />
<div class="container body-content">
@RenderBody()
<hr />
<footer>
<p>© 2018 : Contoso University</p>
</footer>
</div>
@*Remaining markup not shown for brevity.*@
在 Pages/Index.cshtml 中,将文件内容替换为以下代码,以将有关 ASP.NET 和 MVC 的文本替换为有关本应用的文本:
HTML
@page
@model IndexModel
@{
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 Razor Pages web app.
</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.microsoft.com/aspnet/core/data/ef-rp/intro">
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://www.w3cschool.cn/targetlink?url=https://github.com/aspnet/Docs/tree/master/aspnetcore/data/ef-rp/intro/samples/cu-final">
See project source code »
</a>
</p>
</div>
</div>
创建数据模型
创建 Contoso University 应用的实体类。 从以下三个实体开始:
Student 和 Enrollment 实体之间存在一对多关系。 Course 和 Enrollment 实体之间存在一对多关系。 一名学生可以报名参加任意数量的课程。 一门课程中可以包含任意数量的学生。
以下部分将为这几个实体中的每一个实体创建一个类。
Student 实体
创建 Models 文件夹。 在 Models 文件夹中,使用以下代码创建一个名为 Student.cs 的类文件:
C#
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 属性成为此类对应的数据库 (DB) 表的主键列。 默认情况下,EF Core 将名为 ID 或 classnameID 的属性视为主键。 在 classnameID 中,classname 为类名称。 另一种自动识别的主键是上例中的 StudentID。
Enrollments 属性是导航属性。 导航属性链接到与此实体相关的其他实体。 在这种情况下,Student entity 的 Enrollments 属性包含与该 Student 相关的所有 Enrollment 实体。 例如,如果数据库中的 Student 行有两个相关的 Enrollment 行,则 Enrollments 导航属性包含这两个 Enrollment 实体。 相关的 Enrollment 行是 StudentID 列中包含该学生的主键值的行。 例如,假设 ID=1 的学生在 Enrollment 表中有两行。 Enrollment 表中有两行的 StudentID = 1。 StudentID 是 Enrollment 表中的外键,用于指定 Student 表中的学生。
如果导航属性包含多个实体,则导航属性必须是列表类型,例如 ICollection<T>。 可以指定 ICollection<T> 或诸如 List<T> 或 HashSet<T> 的类型。 使用 ICollection<T> 时,EF Core 会默认创建 HashSet<T> 集合。 包含多个实体的导航属性来自于多对多和一对多关系。
Enrollment 实体
在 Models 文件夹中,使用以下代码创建 Enrollment.cs:
C#
namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}
public class Enrollment
{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
public Grade? Grade { get; set; }
public Course Course { get; set; }
public Student Student { get; set; }
}
}
EnrollmentID 属性为主键。 Student 实体使用的是 ID 模式,而本实体使用的是 classnameID 模式。 通常情况下,开发者会选择一种模式并在整个数据模型中都使用该模式。 下一个教程将介绍如何使用不带类名的 ID,以便更轻松地在数据模型中实现集成。
Grade 属性为 enum。 Grade 声明类型后的?表示 Grade 属性可以为 null。 评级为 null 和评级为零是有区别的 --null 意味着评级未知或者尚未分配。
StudentID 属性是外键,其对应的导航属性为 Student。 Enrollment 实体与一个 Student 实体相关联,因此该属性只包含一个 Student 实体。 Student 实体与 Student.Enrollments 导航属性不同,后者包含多个 Enrollment 实体。
CourseID 属性是外键,其对应的导航属性为 Course。 Enrollment 实体与一个 Course 实体相关联。
如果属性命名为 <navigation property name><primary key property name>,EF Core 会将其视为外键。例如 Student 导航属性的 StudentID,因为 Student 实体的主键为 ID。 还可以将外键属性命名为 <primary key property name>。 例如 CourseID,因为 Course 实体的主键为 CourseID。
Course 实体
在 Models 文件夹中,使用以下代码创建 Course.cs:
C#
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 特性指定主键,而无需靠数据库生成。
为“学生”模型搭建基架
本部分将为“学生”模型搭建基架。 确切地说,基架工具将生成页面,用于对“学生”模型执行创建、读取、更新和删除 (CRUD) 操作。
- 生成项目。
- 创建 Pages/Students 文件夹。
- 在“解决方案资源管理器”中,右键单击“Pages/Students”文件夹>“添加”>“新搭建基架的项目”。
- 在“添加基架”对话框中,选择“使用实体框架生成 Razor Pages (CRUD)”>“添加”。
完成“使用实体框架(CRUD)添加 Razor Pages”对话框:
- 在“模型类”下拉列表中,选择“学生(ContosoUniversity.Models)”。
- 在“数据上下文类”行中,选择加号 (+) 并将生成的名称更改为 ContosoUniversity.Models.SchoolContext。
- 在“数据上下文类”下拉列表中,选择“ContosoUniversity.Models.SchoolContext”
- 选择“添加”。
如果对前面的步骤有疑问,请参阅搭建“电影”模型的基架。
搭建基架的过程会创建并更改以下文件:
创建的文件
- Pages/Students:“创建”、“删除”、“详细信息”、“编辑”、“索引”。
- Data/SchoolContext.cs
文件更新
- Startup.cs:下一部分详细介绍对此文件所作的更改。
- appsettings.json:添加用于连接到本地数据库的连接字符串。
检查通过依赖关系注入注册的上下文
ASP.NET Core 通过依赖关系注入进行生成。 服务(例如 EF Core 数据库上下文)在应用程序启动期间通过依赖关系注入进行注册。 需要这些服务(如 Razor 页面)的组件通过构造函数提供相应服务。 本教程的后续部分介绍了用于获取数据库上下文实例的构造函数代码。
基架工具自动创建 DB 上下文并将其注册到依赖关系注入容器。
在 Startup.cs 中检查 ConfigureServices 方法。 基架添加了突出显示的行:
C#
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for
//non -essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddDbContext<SchoolContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("SchoolContext")));
}
通过调用 DbContextOptions 对象中的一个方法将连接字符串名称传递到上下文。 进行本地开发时, ASP.NET Core 配置系统 在 appsettings.json 文件中读取数据库连接字符串。
更新 main
在 Program.cs 中,修改 Main 方法以执行以下操作:
- 从依赖关系注入容器获取数据库上下文实例。
- 调用 EnsureCreated。
- EnsureCreated 方法完成时释放上下文。
下面的代码显示更新后的 Program.cs 文件。
C#
using ContosoUniversity.Models; // SchoolContext
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection; // CreateScope
using Microsoft.Extensions.Logging;
using System;
namespace ContosoUniversity
{
public class Program
{
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>();
context.Database.EnsureCreated();
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB.");
}
}
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
}
EnsureCreated 确保存在上下文数据库。 如果存在,则不需要任何操作。 如果不存在,则会创建数据库及其所有架构。 EnsureCreated 不使用迁移创建数据库。 使用 EnsureCreated 创建的数据库稍后无法使用迁移更新。
启动应用时会调用 EnsureCreated,以进行以下工作流:
- 删除数据库。
- 更改数据库架构(例如添加一个 EmailAddress 字段)。
- 运行应用。
- EnsureCreated 创建一个带有 EmailAddress 列的数据库。
架构快速演变时,在开发初期使用 EnsureCreated 很方便。 本教程后面将删除 DB 并使用迁移。
测试应用
运行应用并接受 cookie 策略。 此应用不保留个人信息。 有关 cookie 策略的信息,请参阅欧盟一般数据保护条例 (GDPR) 支持。
- 依次选择“学生”链接、“新建”。
- 测试“编辑”、“详细信息”和“删除”链接。
检查 SchoolContext DB 上下文
数据库上下文类是为给定数据模型协调 EF Core 功能的主类。 数据上下文派生自 Microsoft.EntityFrameworkCore.DbContext。 数据上下文指定数据模型中包含哪些实体。 在此项目中将数据库上下文类命名为 SchoolContext。
使用以下代码更新 SchoolContext.cs:
C#
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Models
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options)
: base(options)
{
}
public DbSet<Student> Student { get; set; }
public DbSet<Enrollment> Enrollment { get; set; }
public DbSet<Course> Course { get; set; }
}
}
突出显示的代码为每个实体集创建 DbSet<TEntity> 属性。 在 EF Core 术语中:
- 实体集通常对应一个数据库表。
- 实体对应表中的行。
可以省略 DbSet<Enrollment> 和 DbSet<Course>。 EF Core 隐式包含了它们,因为 Student 实体引用 Enrollment 实体,而 Enrollment 实体引用 Course 实体。 在本教程中,将 DbSet<Enrollment>和 DbSet<Course> 保留在 SchoolContext 中。
SQL Server Express LocalDB
连接字符串指定 SQL Server LocalDB。 LocalDB 是轻型版本 SQL Server Express 数据库引擎,专门针对应用开发,而非生产使用。 LocalDB 作为按需启动并在用户模式下运行的轻量级数据库没有复杂的配置。 默认情况下,LocalDB 会在 C:/Users/<user> 目录中创建 .mdf 数据库文件。
添加代码,以使用测试数据初始化该数据库
EF Core 会创建一个空的数据库。 本部分中编写了 Initialize 方法来使用测试数据填充该数据库。
在 Data 文件夹中,新建一个名为 DbInitializer.cs 的类文件,并添加以下代码:
C#
using ContosoUniversity.Models;
using System;
using System.Linq;
namespace ContosoUniversity.Models
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
// context.Database.EnsureCreated();
// Look for any students.
if (context.Student.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.Student.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.Course.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.Enrollment.Add(e);
}
context.SaveChanges();
}
}
}
注意:上面的代码对命名空间使用 Models (namespace ContosoUniversity.Models),而不是 Data。 Models 与基架生成的代码一致。 有关详细信息,请参阅此 GitHub 基架问题。
该代码会检查数据库中是否存在任何学生。 如果 DB 中没有任何学生,则会使用测试数据初始化该 DB。 代码中使用数组存放测试数据而不是使用 List<T> 集合是为了优化性能。
EnsureCreated 方法自动为数据库上下文创建数据库。 如果数据库已存在,则返回 EnsureCreated,并且不修改数据库。
在 Program.cs 中,将 Main 方法修改为调用 Initialize:
C#
public class Program
{
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>();
// using ContosoUniversity.Data;
DbInitializer.Initialize(context);
}
catch (Exception ex)
{
var logger = services.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "An error occurred creating the DB.");
}
}
host.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
删除任何学生记录并重启应用。 如果未初始化 DB,则在 Initialize 中设置断点以诊断问题。
查看数据库
从 Visual Studio 中的“视图”菜单打开 SQL Server 对象资源管理器 (SSOX)。 在 SSOX 中,单击“(localdb)\MSSQLLocalDB”>“数据库”>“ContosoUniversity1”。
展开“表”节点。
右键单击 Student 表,然后单击“查看数据”,以查看创建的列和插入到表中的行。
异步代码
异步编程是 ASP.NET Core 和 EF Core 的默认模式。
Web 服务器的可用线程是有限的,而在高负载情况下的可能所有线程都被占用。 当发生这种情况的时候,服务器就无法处理新请求,直到线程被释放。 使用同步代码时,可能会出现多个线程被占用但不能执行任何操作的情况,因为它们正在等待 I/O 完成。 使用异步代码时,当进程正在等待 I/O 完成,服务器可以将其线程释放用于处理其他请求。 因此,使用异步代码可以更有效地利用服务器资源,并且可以让服务器在没有延迟的情况下处理更多流量。
异步代码会在运行时引入少量开销。 流量较低时,对性能的影响可以忽略不计,但流量较高时,潜在的性能改善非常显著。
在以下代码中,async 关键字和 Task<T> 返回值,await 关键字和 ToListAsync 方法让代码异步执行。
C#
public async Task OnGetAsync()
{
Student = await _context.Student.ToListAsync();
}
- async 关键字让编译器执行以下操作:为方法主体的各部分生成回调。自动创建返回的 Task 对象。 有关详细信息,请参阅任务返回类型。
- 隐式返回类型 Task 表示正在进行的工作。
- await 关键字让编译器将该方法拆分为两个部分。 第一部分是以异步方式结束已启动的操作。第二部分是当操作完成时注入调用回调方法的地方。
- ToListAsync 是 ToList 扩展方法的异步版本。
编写使用 EF Core 的异步代码时需要注意的一些事项:
- 只会异步执行导致查询或命令被发送到数据库的语句。 这包括 ToListAsync、SingleOrDefaultAsync、FirstOrDefaultAsync 和 SaveChangesAsync。 不包括只会更改 IQueryable 的语句,例如 var students = context.Students.Where(s => s.LastName == "Davolio")。
- EF Core 上下文并非线程安全:请勿尝试并行执行多个操作。
- 若要利用异步代码的性能优势,请验证在调用向数据库发送查询的 EF Core 方法时,库程序包(如用于分页)是否使用异步。
有关 .NET 中异步编程的详细信息,请参阅异步概述和使用 Async 和 Await 的异步编程。
下一个教程将介绍基本的 CRUD(创建、读取、更新、删除)操作。