作者:Tom Dykstra 和 Rick Anderson
本文是系列教程的第一篇,这些教程展示如何在 ASP.NET Core Razor Pages 应用中使用 Entity Framework (EF) Core。 这些教程为虚构的 Contoso University 生成一个网站。 网站包括学生录取、课程创建和讲师分配等功能。 本教程使用代码优先方法。 有关使用数据库优先方法学习本教程的信息,请参阅此 Github 问题。
Visual Studio Code 说明使用用于 ASP.NET Core 的 .NET Core CLI 开发功能,如项目创建。 可在任何平台(macOS、Linux 或 Windows)上或在任何代码编辑器中遵循这些说明。 如果使用 Visual Studio Code 以外的其他内容,则可能需要进行少量更改。
Visual Studio 指令使用 SQL Server LocalDB,它是只在 Windows 上运行的一种 SQL Server Express 版本。
Visual Studio Code 指令使用 SQLite,一种跨平台数据库引擎。
如果选择使用 SQLite,请下载并安装适用于 SQLite 的数据库浏览器等第三方工具,用于管理和查看 SQLite 数据库。
如果遇到无法解决的问题,请将你的代码与完成的项目进行比较。 获取帮助的一个好方法是使用 ASP.NET Core 标记或 EF Core 标记将问题发布到 StackOverflow.com。
这些教程中所构建的应用是一个基本的大学网站。 用户可以查看和更新学生、课程和讲师信息。 以下是在本教程中创建的几个屏幕。
此网站的 UI 样式基于内置的项目模板。 本教程侧重于如何使用 EF Core,而不是如何自定义 UI。
单击页面顶部的链接,获取已完成项目的源代码。 “cu30”文件夹中有本教程的 ASP.NET Core 3.0 版本的代码 。 在“cu30snapshots”文件夹中可以找到反映教程 1-7 代码状态的文件 。
若要在下载完成的项目之后运行应用,请执行以下操作:
删除名称中包含 SQLite 的三个文件和一个文件夹 。
生成项目。
在包管理器控制台 (PMC) 中运行以下命令:
Update-Database
运行项目,设定数据库种子。
若要在下载完成的项目之后运行应用,请执行以下操作:
删除 ContosoUniversity.csproj,然后将 ContosoUniversitySQLite.csproj 重命名为 ContosoUniversity.csproj ****** 。
删除 Startup.cs,然后将 StartupSQLite.cs 重命名为 Startup.cs *** *** 。
删除 appSettings.json,然后将 appSettingsSQLite.json 重命名为 appSettings.json ****** 。
删除“Migrations”文件夹,然后将 MigrationsSQL 重命名为 Migrations 。
生成项目。
在项目文件夹中的命令提示符下运行以下命令:
dotnet tool install --global dotnet-ef dotnet ef database update
在 SQLite 工具中,运行以下 SQL 语句:
UPDATE Department SET RowVersion = randomblob(8)
运行项目,设定数据库种子。
在终端中,导航到应在其中创建项目文件夹的文件夹。
运行以下命令,在新的项目文件夹中创建 Razor Pages 项目和 cd
:
dotnet new webapp -o ContosoUniversity cd ContosoUniversity
更新 Pages/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-page="/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-page="/About">About</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/Students/Index">Students</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/Courses/Index">Courses</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/Instructors/Index">Instructors</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-page="/Departments/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"> © 2019 - Contoso University - <a asp-area="" asp-page="/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> @RenderSection("Scripts", required: false) </body> </html>
在 Pages/Index.cshtml 中,将文件内容替换为以下代码,以将有关 ASP.NET Core 的文本替换为有关本应用的文本 :
@page @model IndexModel @{ ViewData["Title"] = "Home page"; } <div class="row mb-auto"> <div class="col-md-4"> <div class="row no-gutters border mb-4"> <div class="col p-4 mb-4 "> <p class="card-text"> 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> </div> <div class="col-md-4"> <div class="row no-gutters border mb-4"> <div class="col p-4 d-flex flex-column position-static"> <p class="card-text mb-auto"> You can build the application by following the steps in a series of tutorials. </p> <p> <a href="https://docs.microsoft.com/aspnet/core/data/ef-rp/intro" class="stretched-link">See the tutorial</a> </p> </div> </div> </div> <div class="col-md-4"> <div class="row no-gutters border mb-4"> <div class="col p-4 d-flex flex-column"> <p class="card-text mb-auto"> You can download the completed project from GitHub. </p> <p> <a href="https://github.com/dotnet/AspNetCore.Docs/tree/master/aspnetcore/data/ef-rp/intro/samples" class="stretched-link">See project source code</a> </p> </div> </div> </div> </div>
运行应用以验证主页是否显示。
以下部分用于创建数据模型:
一名学生可以修读任意数量的课程,并且某一课程可以有任意数量的学生修读。
在项目文件夹中创建“Models”文件夹 。
使用以下代码创建 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 Core 将名为 ID
或 classnameID
的属性视为主键。 因此,Student
类主键的另一种自动识别的名称是 StudentID
。
Enrollments
属性是导航属性。 导航属性中包含与此实体相关的其他实体。 在本例中,Student
实体的 Enrollments
属性包含与该 Student 相关的所有 Enrollment
实体。 例如,如果数据库中的 Student 行有两个相关的 Enrollment 行,则 Enrollments
导航属性包含这两个 Enrollment 实体。
在数据库中,如果 StudentID 列包含学生的 ID 值,则 Enrollment 行与 Student 行相关。 例如,假设某个 Student 行的 ID=1。 则相关 Enrollment 行的 StudentID = 1。 StudentID 是 Enrollment 表中的外键 。
Enrollments
属性定义为 ICollection<Enrollment>
,因为可能有多个相关的 Enrollment 实体。 可以使用 List<Enrollment>
或 HashSet<Enrollment>
等其他集合类型。 使用 ICollection<Enrollment>
时,EF Core 会默认创建 HashSet<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
模式而不是直接使用 ID
。 对于生产数据模型,请选择一个模式并一直使用。 本教程两个都使用,只是为了说明这两个模式都能使用。 使用不具有 classname
的 ID
可以更轻松地实现某些类型的数据模型更改。
Grade
属性为 enum
。 Grade
声明类型后的?
表示 Grade
属性可以为 null。 评级为 null 和评级为零是有区别的 — null 意味着评级未知或者尚未分配。
StudentID
属性是外键,其对应的导航属性为 Student
。 Enrollment
实体与一个 Student
实体相关联,因此该属性只包含一个 Student
实体。
CourseID
属性是外键,其对应的导航属性为 Course
。 Enrollment
实体与一个 Course
实体相关联。
如果属性命名为 <navigation property name><primary key property name>
,EF Core 会将其视为外键。 例如,StudentID
是 Student
导航属性的外键,因为 Student
实体的主键为 ID
。 还可以将外键属性命名为 <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
特性指定主键,而无需靠数据库生成。
生成项目以验证没有任何编译器错误。
本部分使用 ASP.NET Core 基架工具生成以下内容:
Microsoft.EntityFrameworkCore.DbContext
类。Student
实体的创建、读取、更新和删除 (CRUD) 操作。自动安装以下包:
Microsoft.VisualStudio.Web.CodeGeneration.Design
Microsoft.EntityFrameworkCore.SqlServer
Microsoft.Extensions.Logging.Debug
Microsoft.EntityFrameworkCore.Tools
dotnet add package Microsoft.EntityFrameworkCore.SQLite dotnet add package Microsoft.EntityFrameworkCore.SqlServer dotnet add package Microsoft.EntityFrameworkCore.Design dotnet add package Microsoft.EntityFrameworkCore.Tools dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design dotnet add package Microsoft.Extensions.Logging.Debug
基架需要 Microsoft.VisualStudio.Web.CodeGeneration.Design 包。 虽然应用不使用 SQL Server,但基架工具需要 SQL Server 包。
创建“Pages/Students”文件夹 。
运行以下命令安装 aspnet-codegenerator 基架工具。
dotnet tool install --global dotnet-aspnet-codegenerator
运行以下命令,搭建“学生”页的基架。
在 Windows 上
dotnet aspnet-codegenerator razorpage -m Student -dc ContosoUniversity.Data.SchoolContext -udl -outDir Pages\Students --referenceScriptLibraries
在 macOS 或 Linux 上
dotnet aspnet-codegenerator razorpage -m Student -dc ContosoUniversity.Data.SchoolContext -udl -outDir Pages/Students --referenceScriptLibraries
如果对上述步骤有疑问,请生成项目并重试基架搭建步骤。
基架流程:
连接字符串指定 SQL Server LocalDB。
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "ConnectionStrings": { "SchoolContext": "Server=(localdb)\\mssqllocaldb;Database=SchoolContext;Trusted_Connection=True;MultipleActiveResultSets=true" } }
LocalDB 是轻型版本 SQL Server Express 数据库引擎,专门针对应用开发,而非生产使用。 默认情况下,LocalDB 会在 C:/Users/<user>
目录中创建 .mdf 文件 。
更改连接字符串以指向名为 CU.db 的 SQLite 数据库文件 :
{ "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", "ConnectionStrings": { "SchoolContext": "Data Source=CU.db" } }
数据库上下文类是为给定数据模型协调 EF Core 功能的主类。 上下文派生自 Microsoft.EntityFrameworkCore.DbContext。 上下文指定数据模型中包含哪些实体。 在此项目中将数据库上下文类命名为 SchoolContext
。
使用以下代码更新 SchoolContext.cs :
using Microsoft.EntityFrameworkCore; using ContosoUniversity.Models; namespace ContosoUniversity.Data { public class SchoolContext : DbContext { public SchoolContext (DbContextOptions<SchoolContext> options) : base(options) { } public DbSet<Student> Students { get; set; } public DbSet<Enrollment> Enrollments { get; set; } public DbSet<Course> Courses { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Course>().ToTable("Course"); modelBuilder.Entity<Enrollment>().ToTable("Enrollment"); modelBuilder.Entity<Student>().ToTable("Student"); } } }
突出显示的代码为每个实体集创建 DbSet<TEntity> 属性。 在 EF Core 术语中:
由于实体集包含多个实体,因此 DBSet 属性应为复数名称。 由于基架工具创建了 Student
DBSet,因此此步骤将其更改为复数 Students
。
为了使 Razor Pages 代码与新的 DBSet 名称相匹配,请在整个项目中进行全局更改,将 _context.Student
更改为 _context.Students
。 更改发生 8 次。
生成项目以验证没有任何编译器错误。
ASP.NET Core 通过依赖关系注入进行生成。 在应用程序启动过程通过依赖注入注册相关服务(例如 EF Core 数据库上下文)。 需要这些服务(如 Razor 页面)的组件通过构造函数提供相应服务。 本教程的后续部分介绍了用于获取数据库上下文实例的构造函数代码。
基架工具自动将上下文类注册到了依赖项注入容器。
在 ConfigureServices
中,基架添加了突出显示的行:
public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddDbContext<SchoolContext>(options => options.UseSqlServer(Configuration.GetConnectionString("SchoolContext"))); }
在 ConfigureServices
中,确保基架所添加的代码调用 UseSqlite
。
public void ConfigureServices(IServiceCollection services) { services.AddRazorPages(); services.AddDbContext<SchoolContext>(options => options.UseSqlite(Configuration.GetConnectionString("SchoolContext"))); }
通过调用 DbContextOptions 对象中的一个方法将连接字符串名称传递到上下文。 进行本地开发时, ASP.NET Core 配置系统 在 appsettings.json 文件中读取数据库连接字符串。
如果没有数据库,请更新 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>(); context.Database.EnsureCreated(); } 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>(); }); } }
如果有上下文的数据库,则 EnsureCreated 方法不执行任何操作。 如果没有数据库,则它将创建数据库和架构。 EnsureCreated
启用以下工作流来处理数据模型更改:
EmailAddress
字段。EnsureCreated
创建具有新架构的数据库。在无需保存数据的情况下,当架构快速发展时,此工作流在早期开发过程中表现良好。 如果需要保存已输入数据库的数据,情况就有所不同了。 如果是这种情况,请使用迁移。
本系列教程的后续部分将删除 EnsureCreated
创建的数据库,转而使用迁移。 无法使用迁移更新 EnsureCreated
创建的数据库。
EnsureCreated
方法将创建空数据库。 本节添加用测试数据填充数据库的代码。
使用以下代码创建 Data/DbInitializer.cs :
using ContosoUniversity.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("2019-09-01")}, new Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Parse("2017-09-01")}, new Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse("2018-09-01")}, new Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Parse("2017-09-01")}, new Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2017-09-01")}, new Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Parse("2016-09-01")}, new Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse("2018-09-01")}, new Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Parse("2019-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 中,将 EnsureCreated
调用替换为 DbInitializer.Initialize
调用 :
// context.Database.EnsureCreated(); DbInitializer.Initialize(context);
如果应用正在运行,则停止应用,然后在包管理器控制台 (PMC) 中运行以下命令 :
Drop-Database
重新启动应用。
选择“学生”页查看已设定种子的数据。
Student
模型如何映射到 Student
表架构 。使用 SQLite 工具查看数据库架构和已设定种子的数据。 数据库文件名为 CU.db,位于项目文件夹 。
异步编程是 ASP.NET Core 和 EF Core 的默认模式。
Web 服务器的可用线程是有限的,而在高负载情况下的可能所有线程都被占用。 当发生这种情况的时候,服务器就无法处理新请求,直到线程被释放。 使用同步代码时,可能会出现多个线程被占用但不能执行任何操作的情况,因为它们正在等待 I/O 完成。 使用异步代码时,当进程正在等待 I/O 完成,服务器可以将其线程释放用于处理其他请求。 因此,使用异步代码可以更有效地利用服务器资源,并且服务器可以无延迟地处理更多流量。
异步代码会在运行时引入少量开销。 流量较低时,对性能的影响可以忽略不计,但流量较高时,潜在的性能改善非常显著。
在以下代码中,async 关键字和 Task<T>
返回值,await
关键字和 ToListAsync
方法让代码异步执行。
public async Task OnGetAsync() { Students = await _context.Students.ToListAsync(); }
async
关键字让编译器执行以下操作:
Task<T>
表示正在进行的工作。await
关键字让编译器将该方法拆分为两个部分。 第一部分是以异步方式结束已启动的操作。 第二部分是当操作完成时注入调用回调方法的地方。ToListAsync
是 ToList
扩展方法的异步版本。编写使用 EF Core 的异步代码时需要注意的一些事项:
ToListAsync
、SingleOrDefaultAsync
、FirstOrDefaultAsync
和 SaveChangesAsync
。 不包括只会更改 IQueryable
的语句,例如 var students = context.Students.Where(s => s.LastName == "Davolio")
。有关 .NET 中异步编程的详细信息,请参阅异步概述和使用 Async 和 Await 的异步编程。