ASP.NET Core 中的 Razor Pages 和 Entity Framework Core - 第 1 个教程(共 8 个)

Note

此版本不是本文的最新版本。 有关当前版本,请参阅 本文的 .NET 10 版本

Warning

此版本的 ASP.NET Core 不再受支持。 有关详细信息,请参阅 .NET 和 .NET Core 支持策略。 有关当前版本,请参阅 本文的 .NET 10 版本

作者:Tom DykstraJeremy LiknessJon P Smith

本文是系列教程的第一篇,这些教程展示如何在 ASP.NET Core Razor Pages 应用中使用 Entity Framework (EF) Core。 这些教程将为一所虚构的 Contoso University 构建一个网站。 网站包括学生录取、课程创建和讲师分配等功能。 本教程使用代码优先方法。 有关使用数据库优先方法学习本教程的信息,请参阅此 Github 问题

下载或查看已完成的应用。下载说明

Prerequisites

  • 如果不熟悉 Razor Pages,则在开始前,请浏览 Razor Pages 入门系列教程。

数据库引擎

Visual Studio 指令使用 SQL Server LocalDB,它是只在 Windows 上运行的一种 SQL Server Express 版本。

Troubleshooting

如果遇到无法解决的问题,请将你的代码与完成的项目进行比较。 获取帮助的一个好方法是使用 ASP.NET Core 标记EF Core 标记将问题发布到 StackOverflow.com。

示例应用

这些教程中所构建的应用是一个基本的大学网站。 用户可以查看和更新学生、课程和讲师信息。 以下是在本教程中创建的几个屏幕。

“学生索引”页

学生编辑页

此网站的 UI 样式基于内置的项目模板。 本教程侧重于如何将 EF Core 和 ASP.NET Core 结合使用,而不是如何自定义 UI。

可选:生成示例下载

此步骤是可选的。 如果遇到无法解决的问题,建议生成已完成的应用。 如果遇到无法解决的问题,请将你的代码与完成的项目进行比较。 下载说明

选择 ContosoUniversity.csproj 打开项目。

  • 构建项目。

  • 在包管理器控制台 (PMC) 中运行以下命令:

    Update-Database
    

运行项目以初始化数据库数据。

创建 Web 应用项目

  1. 启动 Visual Studio 2022 并选择“创建新项目”。

    从“启动”窗口创建新项目

  2. 在“创建新项目”对话框中,选择“ASP.NET Core Web 应用”,然后选择“下一步” 。

    创建 ASP.NET Core Web 应用

  3. 在“配置新项目”对话框中,为“项目名称”输入 。 请务必将项目命名为 ContosoUniversity(包括匹配大小写),这样在复制和粘贴代码时命名空间就会匹配。

  4. 选择“下一步”。

  5. 在“其他信息”对话框中,选择“.NET 6.0 (长期支持)”,然后选择“创建”。

    其他信息

设置网站样式

将以下代码复制并粘贴到 Pages/Shared/_Layout.cshtml 文件中:

<!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" asp-append-version="true" />
    <link rel="stylesheet" href="~/ContosoUniversity.styles.css" asp-append-version="true" />
</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-bs-toggle="collapse" data-bs-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-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">
            &copy; 2021 - 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>

    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

布局文件会设置网站页眉、页脚和菜单。 上面的代码执行以下更改:

  • 将每个“ContosoUniversity”替换为“Contoso University”。 有三处。
  • HomePrivacy 条目被删除。
  • 已添加 关于学生课程教师院系 条目。

Pages/Index.cshtml 中,将文件的内容替换为以下代码:

@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/main/aspnetcore/data/ef-rp/intro/samples" class="stretched-link">See project source code</a>
*@                </p>
            </div>
        </div>
    </div>
</div>

前面的代码会将关于 ASP.NET Core 的文本替换为关于此应用的文本。

运行应用以验证主页是否显示。

数据模型

以下部分用于创建数据模型:

Course-Enrollment-Student 数据模型关系图

一名学生可以修读任意数量的课程,并且某一课程可以有任意数量的学生修读。

学生实体

学生实体图

  • 在项目文件夹中创建“Models”文件夹。
  • 使用以下代码创建 Models/Student.cs
    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 将名为 IDclassnameID 的属性解释为主键。 因此,Student 类主键的另一种自动识别的名称是 StudentID。 有关详细信息,请参阅 EF Core - 键

Enrollments 属性是导航属性。 导航属性中包含与此实体相关的其他实体。 在本例中,Enrollments 实体的 Student 属性包含与该 Student 相关的所有 Enrollment 实体。 例如,如果数据库中的 Student 行有两个相关的 Enrollment 行,则 Enrollments 导航属性包含这两个 Enrollment 实体。

在数据库中,如果 StudentID 列包含学生的 ID 值,则 Enrollment 行与 Student 行相关。 例如,假设 Student 表中的一行的 ID 为 1。 相关注册行的 StudentID = 1。 StudentID 是注册表中的外键

Enrollments 属性被定义为 ICollection<Enrollment>,因为可能存在多个相关的注册实体。 可以使用 List<Enrollment>HashSet<Enrollment> 等其他集合类型。 使用 ICollection<Enrollment> 时,EF Core 默认创建 HashSet<Enrollment> 集合。

注册实体

选课实体图

使用以下代码创建 Models/Enrollment.cs

using System.ComponentModel.DataAnnotations;

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; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}

EnrollmentID 属性为主键;此实体使用 classnameID 模式而不是直接使用 ID。 对于生产数据模型,许多开发人员会选择一个模式,并一直使用它。 本教程同时使用这两种方式,只是为了说明两者都可行。 使用不具有 IDclassname 可以更轻松地实现某些类型的数据模型更改。

Grade 属性为 enumGrade 类型声明后的问号(`?`)表示 Grade 属性可为 null。 值为 null 的分数与零分不同——null 表示该分数未知或尚未给出。

StudentID 属性是外键,其对应的导航属性为 StudentEnrollment 实体与一个 Student 实体相关联,因此该属性只包含一个 Student 实体。

CourseID 属性是外键,其对应的导航属性为 CourseEnrollment 实体与一个 Course 实体相关联。

如果属性命名为 EF Core,<navigation property name><primary key property name> 会将其视为外键。 例如,StudentIDStudent 导航属性的外键,因为 Student 实体的主键为 ID。 还可以将外键属性命名为 <primary key property name>。 例如 CourseID,因为 Course 实体的主键为 CourseID

课程实体

课程实体图

使用以下代码创建 Models/Course.cs

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 特性指定主键,而无需靠数据库生成。

构建应用程序。 编译器会生成多个有关如何处理 null 值的警告。 有关详细信息,请参阅此 GitHub 问题可为空引用类型教程:使用可为空和不可为空引用类型更清晰地表达设计意图

若要消除可为空引用类型的警告,请从 ContosoUniversity.csproj 文件中删除以下行:

<Nullable>enable</Nullable>

基架引擎当前不支持可为空引用类型,因此基架中使用的模型也不支持。

Pages/Error.cshtml.cs 中的 public string? RequestId { get; set; } 删除 ? 可为空引用类型注释,以便项目在生成时不出现编译器警告。

搭建学生页面框架

本部分使用 ASP.NET Core 基架工具生成以下内容:

  • 一个 EF CoreDbContext 类。 上下文是为给定数据模型协调实体框架功能的主类。 它派生自 Microsoft.EntityFrameworkCore.DbContext 类。
  • Razor 页面,可处理 Student 实体的创建、读取、更新和删除 (CRUD) 操作。
  • 创建“Pages/Students”文件夹。
  • “解决方案资源管理器”中,右键单击“Pages/Students”文件夹,然后选择“添加”>“新建基架项”。
  • 在“添加新基架项”对话框中:
    • 在左侧选项卡中,选择已安装>常用>Razor页面
    • 选择Razor使用 Entity Framework (CRUD) 的页面>ADD
  • “添加Razor使用 Entity Framework 的页面 (CRUD)”对话框中:
    • 在“模型类”下拉列表中,选择“Student (ContosoUniversity.Models)” 。
    • 在“数据上下文类”行中,选择 +(加号)。
      • 将数据上下文名称更改为以 SchoolContext 结尾,而不以 ContosoUniversityContext 结尾。 更新后的上下文名称为:ContosoUniversity.Data.SchoolContext
      • 选择“添加”,完成数据上下文类的添加。
      • 选择“添加”以完成“添加 Razor 页面”对话框中的操作。

自动安装以下包:

  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools
  • Microsoft.VisualStudio.Web.CodeGeneration.Design

如果上述步骤失败,请生成项目并重试基架搭建步骤。

基架流程:

  • 在“Pages/Students”Razor文件夹中创建 页面:
    • Create.cshtmlCreate.cshtml.cs
    • Delete.cshtmlDelete.cshtml.cs
    • Details.cshtmlDetails.cshtml.cs
    • Edit.cshtmlEdit.cshtml.cs
    • Index.cshtmlIndex.cshtml.cs
  • 创建 Data/SchoolContext.cs
  • Program.cs 中为依赖注入添加上下文。
  • 将数据库连接字符串添加到 appsettings.json

数据库连接字符串

基架工具会在 appsettings.json 文件中生成连接字符串。

此连接字符串指定了 SQL Server LocalDB

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "SchoolContext": "Server=(localdb)\\mssqllocaldb;Database=SchoolContext-0e9;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

LocalDB 是轻型版本 SQL Server Express 数据库引擎,专门针对应用开发,而非生产使用。 默认情况下,LocalDB 会在 目录中创建 .mdf 文件。

更新数据库上下文类

数据库上下文类是为给定数据模型协调 EF Core 功能的主类。 上下文派生自 Microsoft.EntityFrameworkCore.DbContext。 上下文指定数据模型中包含哪些实体。 在此项目中,该类名为 SchoolContext

使用以下代码更新 Data/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<Student> Student 更改为复数形式的 DbSet<Student> Students。 若要使 Razor 页面代码与新的 DBSet 名称匹配,请从以下项进行全局更改:将 _context.Student. 更改为:_context.Students.

共出现 8 次。

由于一个实体集包含多个实体,因此许多开发人员更倾向于使用复数形式的 DBSet 属性名称。

突出显示的代码:

  • 将为每个实体集创建一个 DbSet<TEntity> 属性。 用 EF Core 的术语来说:
    • 实体集通常对应数据库表。
    • 一个实体对应于表中的一行。
  • 调用 OnModelCreatingOnModelCreating
    • SchoolContext 已初始化之后、但在模型受到保护并用于初始化上下文之前调用。
    • 是必需的,因为在本教程的后续部分中,Student 实体将引用其他实体。

我们希望在将来的版本中修复此问题

Program.cs

ASP.NET Core 通过依赖关系注入进行生成。 服务(例如 SchoolContext)在应用程序启动期间通过依赖关系注入进行注册。 需要这些服务(如 Razor 页面)的组件通过构造函数参数提供相应服务。 本教程的后续部分介绍了用于获取数据库上下文实例的构造函数代码。

基架工具自动将上下文类注册到了依赖项注入容器。

以下高亮显示的行由脚手架工具添加:

using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDbContext<SchoolContext>(options =>
  options.UseSqlServer(builder.Configuration.GetConnectionString("SchoolContext")));

通过在 DbContextOptions 对象上调用一个方法,将连接字符串的名称传入上下文。 进行本地开发时,ASP.NET Core 配置系统会从 appsettings.jsonappsettings.Development.json 文件中读取连接字符串。

添加数据库异常筛选器

添加 AddDatabaseDeveloperPageExceptionFilterUseMigrationsEndPoint,如下面的代码所示:

using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDbContext<SchoolContext>(options =>
  options.UseSqlServer(builder.Configuration.GetConnectionString("SchoolContext")));

builder.Services.AddDatabaseDeveloperPageExceptionFilter();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
else
{
    app.UseDeveloperExceptionPage();
    app.UseMigrationsEndPoint();
}

添加 Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore NuGet 包。

在包管理器控制台中,输入以下命令来添加 NuGet 包:

Install-Package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore

Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore NuGet 包提供用于 Entity Framework Core 错误页的 ASP.NET Core 中间件。 此中间件有助于检测和诊断 Entity Framework Core 迁移错误。

AddDatabaseDeveloperPageExceptionFilter开发环境中为 EF 迁移错误提供有用的错误信息。

创建数据库

如果没有数据库,请更新 Program.cs 以创建数据库:

using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddRazorPages();

builder.Services.AddDbContext<SchoolContext>(options =>
  options.UseSqlServer(builder.Configuration.GetConnectionString("SchoolContext")));

builder.Services.AddDatabaseDeveloperPageExceptionFilter();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}
else
{
    app.UseDeveloperExceptionPage();
    app.UseMigrationsEndPoint();
}

using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    var context = services.GetRequiredService<SchoolContext>();
    context.Database.EnsureCreated();
    // DbInitializer.Initialize(context);
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthorization();

app.MapRazorPages();

app.Run();

如果有上下文的数据库,则 EnsureCreated 方法不执行任何操作。 如果没有数据库,则它将创建数据库和架构。 EnsureCreated 启用以下工作流来处理数据模型更改:

  • 删除数据库。 任何现有数据丢失。
  • 更改数据模型。 例如,添加 EmailAddress 字段。
  • 运行应用。
  • EnsureCreated 创建具有新架构的数据库。

在无需保存数据的情况下,当架构快速发展时,此工作流在早期开发过程中表现良好。 如果需要保存已输入数据库的数据,情况就有所不同了。 在这种情况下,请使用迁移功能。

本系列教程的后续部分将删除 EnsureCreated 创建的数据库,转而使用迁移。 由 EnsureCreated 创建的数据库无法通过迁移进行更新。

测试应用

  • 运行应用。
  • 选择 学生 链接,然后选择 创建新项
  • 测试“编辑”、“详细信息”和“删除”链接。

填充数据库初始数据

EnsureCreated 方法将创建空数据库。 本节添加用测试数据填充数据库的代码。

使用以下代码创建 Data/DbInitializer.cs

using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            // 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")}
            };

            context.Students.AddRange(students);
            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}
            };

            context.Courses.AddRange(courses);
            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},
            };

            context.Enrollments.AddRange(enrollments);
            context.SaveChanges();
        }
    }
}

该代码会检查数据库中是否存在任何学生。 如果不存在学生,它将向数据库添加测试数据。 该代码使用数组创建测试数据而不是使用 List<T> 集合是为了优化性能。

  • Program.cs 中,从 // 行中删除 DbInitializer.Initialize
using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;

    var context = services.GetRequiredService<SchoolContext>();
    context.Database.EnsureCreated();
    DbInitializer.Initialize(context);
}
  • 如果应用正在运行,则停止应用,然后在包管理器控制台 (PMC) 中运行以下命令:

    Drop-Database -Confirm
    
    
  • 使用 Y 进行响应,以删除数据库。

  • 重新启动应用。
  • 选择“学生”页以查看预填充数据。

查看数据库

  • 从 Visual Studio 中的“视图”菜单打开 SQL Server 对象资源管理器 (SSOX) 。
  • 在 SSOX 中,依次选择 “(localdb)\MSSQLLocalDB”>“数据库”>“SchoolContext-{GUID}”。 数据库名称由先前提供的上下文名称再加上一个短划线和一个 GUID 生成。
  • 展开 Tables 节点。
  • 右键单击 Student 表,然后单击“查看数据”,以查看创建的列和插入到表中的行 。
  • 右键单击 Student 表,然后单击 View Code,查看 Student 模型如何映射到 Student 表架构。

ASP.NET Core Web 应用中的异步 EF 方法

异步编程是 ASP.NET Core 和 EF Core 的默认模式。

Web 服务器的可用线程是有限的,而在高负载情况下的可能所有线程都被占用。 当发生这种情况的时候,服务器就无法处理新请求,直到线程被释放。 使用同步代码时,可能会出现多个线程被占用但不能执行操作的情况,因为它们正在等待 I/O 完成。 使用异步代码时,当进程正在等待 I/O 完成,服务器可以将其线程释放用于处理其他请求。 因此,使用异步代码可以更有效地利用服务器资源,并且服务器可以无延迟地处理更多流量。

异步代码会在运行时引入少量开销。 流量较低时,对性能的影响可以忽略不计,但流量较高时,潜在的性能改善非常显著。

在以下代码中,async 关键字和 Task 返回值,await 关键字和 ToListAsync 方法让代码异步执行。

public async Task OnGetAsync()
{
    Students = await _context.Students.ToListAsync();
}
  • async 关键字让编译器执行以下操作:
    • 为方法体的各个部分生成回调。
    • 创建返回的 Task 对象。
  • 返回类型 Task 表示正在进行的工作。
  • await 关键字让编译器将该方法拆分为两个部分。 第一部分以异步启动的操作结束。 第二部分被放入一个在操作完成时调用的回调方法中。
  • ToListAsyncToList 扩展方法的异步版本。

编写使用 EF Core 的异步代码时需要注意的一些事项:

  • 只有导致查询或发送数据库命令的语句才能以异步方式执行。 这包括 ToListAsyncSingleOrDefaultAsyncFirstOrDefaultAsyncSaveChangesAsync。 不包括只会更改 IQueryable 的语句,例如 var students = context.Students.Where(s => s.LastName == "Davolio")
  • EF Core 上下文不是线程安全的:请勿尝试并行执行多个操作。
  • 若要利用异步代码的性能优势,请验证在调用向数据库发送查询的 EF Core 方法时,库程序包(例如用于分页)是否使用异步。

有关 .NET 中异步编程的详细信息,请参阅异步概述使用 Async 和 Await 的异步编程

Warning

Microsoft.Data.SqlClient 的异步实现存在一些已知问题(例如 #593#601 等)。 如果遇到意外的性能问题,请尝试改用同步命令执行,尤其是在处理大型文本或二进制值时。

性能注意事项

通常,网页不应加载任何数量的行。 查询应使用分页或限制结果数量的方式。 例如,上述查询可以使用 Take 来限制返回的行数:

public async Task OnGetAsync()
{
    Student = await _context.Students.Take(10).ToListAsync();
}

如果在枚举进行到一部分时发生数据库异常,那么在视图中枚举大型表可能会返回部分生成的 HTTP 200 响应。

稍后将在教程中介绍分页。

有关详细信息,请参阅性能注意事项 (EF)

后续步骤

将 SQLite 用于开发,将 SQL Server 用于生产

本文是系列教程的第一篇,这些教程展示如何在 ASP.NET Core Razor Pages 应用中使用 Entity Framework (EF) Core。 这些教程将为一所虚构的 Contoso University 构建一个网站。 网站包括学生录取、课程创建和讲师分配等功能。 本教程使用代码优先方法。 有关使用数据库优先方法学习本教程的信息,请参阅此 Github 问题

下载或查看已完成的应用。下载说明

Prerequisites

  • 如果不熟悉 Razor Pages,则在开始前,请浏览 Razor Pages 入门系列教程。

数据库引擎

Visual Studio 指令使用 SQL Server LocalDB,它是只在 Windows 上运行的一种 SQL Server Express 版本。

Troubleshooting

如果遇到无法解决的问题,请将你的代码与完成的项目进行比较。 获取帮助的一个好方法是使用 ASP.NET Core 标记EF Core 标记将问题发布到 StackOverflow.com。

示例应用

这些教程中所构建的应用是一个基本的大学网站。 用户可以查看和更新学生、课程和讲师信息。 以下是在本教程中创建的几个屏幕。

“学生索引”页

学生编辑页

此网站的 UI 样式基于内置的项目模板。 本教程侧重于如何将 EF Core 和 ASP.NET Core 结合使用,而不是如何自定义 UI。

可选:生成示例下载

此步骤是可选的。 如果遇到无法解决的问题,建议生成已完成的应用。 如果遇到无法解决的问题,请将你的代码与完成的项目进行比较。 下载说明

选择 ContosoUniversity.csproj 打开项目。

  • 构建项目。
  • 在包管理器控制台 (PMC) 中运行以下命令:
Update-Database

运行项目以初始化数据库数据。

创建 Web 应用项目

  1. 启动 Visual Studio 并选择“创建新项目”。
  2. 在“新建项目”对话框中,选择“ASP.NET Core Web 应用程序”“下一步”。
  3. 在“配置新项目”对话框中,为“项目名称”输入 。 请务必使用此名称(含大写),确保在复制代码时与每个 namespace 都相匹配。
  4. 选择 创建
  5. 在“创建新的 ASP.NET Core Web 应用程序”对话框中,选择:
    1. 在下拉列表中选择.NET CoreASP.NET Core 5.0
    2. ASP.NET Core Web 应用
    3. 创建新的 ASP.NET Core 项目对话框

设置网站样式

将以下代码复制并粘贴到 Pages/Shared/_Layout.cshtml 文件中:

<!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">
            &copy; 2021 - 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>

布局文件会设置网站页眉、页脚和菜单。 上面的代码执行以下更改:

  • 将每个“ContosoUniversity”替换为“Contoso University”。 有三处。
  • HomePrivacy 条目被删除。
  • 已添加 关于学生课程教师院系 条目。

Pages/Index.cshtml 中,将文件的内容替换为以下代码:

@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/main/aspnetcore/data/ef-rp/intro/samples" class="stretched-link">See project source code</a>
                </p>
            </div>
        </div>
    </div>
</div>

前面的代码会将关于 ASP.NET Core 的文本替换为关于此应用的文本。

运行应用以验证主页是否显示。

数据模型

以下部分用于创建数据模型:

Course-Enrollment-Student 数据模型关系图

一名学生可以修读任意数量的课程,并且某一课程可以有任意数量的学生修读。

学生实体

学生实体图

  • 在项目文件夹中创建“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 将名为 IDclassnameID 的属性解释为主键。 因此,Student 类主键的另一种自动识别的名称是 StudentID。 有关详细信息,请参阅 EF Core - 键

Enrollments 属性是导航属性。 导航属性中包含与此实体相关的其他实体。 在本例中,Enrollments 实体的 Student 属性包含与该 Student 相关的所有 Enrollment 实体。 例如,如果数据库中的 Student 行有两个相关的 Enrollment 行,则 Enrollments 导航属性包含这两个 Enrollment 实体。

在数据库中,如果 StudentID 列包含学生的 ID 值,则 Enrollment 行与 Student 行相关。 例如,假设 Student 表中的一行的 ID 为 1。 相关注册行的 StudentID = 1。 StudentID 是注册表中的外键

Enrollments 属性被定义为 ICollection<Enrollment>,因为可能存在多个相关的注册实体。 可以使用 List<Enrollment>HashSet<Enrollment> 等其他集合类型。 使用 ICollection<Enrollment> 时,EF Core 默认创建 HashSet<Enrollment> 集合。

注册实体

选课实体图

使用以下代码创建 Models/Enrollment.cs

using System.ComponentModel.DataAnnotations;

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; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public Course Course { get; set; }
        public Student Student { get; set; }
    }
}

EnrollmentID 属性为主键;此实体使用 classnameID 模式而不是直接使用 ID。 对于生产数据模型,许多开发人员会选择一个模式,并一直使用它。 本教程同时使用这两种方式,只是为了说明两者都可行。 使用不具有 IDclassname 可以更轻松地实现某些类型的数据模型更改。

Grade 属性为 enumGrade 类型声明后的问号(`?`)表示 Grade 属性可为 null。 值为 null 的分数与零分不同——null 表示该分数未知或尚未给出。

StudentID 属性是外键,其对应的导航属性为 StudentEnrollment 实体与一个 Student 实体相关联,因此该属性只包含一个 Student 实体。

CourseID 属性是外键,其对应的导航属性为 CourseEnrollment 实体与一个 Course 实体相关联。

如果属性命名为 EF Core,<navigation property name><primary key property name> 会将其视为外键。 例如,StudentIDStudent 导航属性的外键,因为 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 基架工具生成以下内容:

  • 一个 EF CoreDbContext 类。 上下文是为给定数据模型协调实体框架功能的主类。 它派生自 Microsoft.EntityFrameworkCore.DbContext 类。
  • Razor 页面,可处理 Student 实体的创建、读取、更新和删除 (CRUD) 操作。
  • 创建“Pages/Students”文件夹。
  • “解决方案资源管理器”中,右键单击“Pages/Students”文件夹,然后选择“添加”>“新建基架项”。
  • 在“添加新基架项”对话框中:
    • 在左侧选项卡中,选择已安装>常用>Razor页面
    • 选择Razor使用 Entity Framework (CRUD) 的页面>ADD
  • “添加Razor使用 Entity Framework 的页面 (CRUD)”对话框中:
    • 在“模型类”下拉列表中,选择“Student (ContosoUniversity.Models)” 。
    • 在“数据上下文类”行中,选择 +(加号)。
      • 将数据上下文名称更改为以 SchoolContext 结尾,而不以 ContosoUniversityContext 结尾。 更新后的上下文名称为:ContosoUniversity.Data.SchoolContext
      • 选择“添加”,完成数据上下文类的添加。
      • 选择“添加”以完成“添加 Razor 页面”对话框中的操作。

如果基架失败并出现错误 'Install the package Microsoft.VisualStudio.Web.CodeGeneration.Design and try again.',请再次运行基架工具,或者请参阅此 GitHub 问题

自动安装以下包:

  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools
  • Microsoft.VisualStudio.Web.CodeGeneration.Design

如果上述步骤失败,请生成项目并重试基架搭建步骤。

基架流程:

  • 在“Pages/Students”Razor文件夹中创建 页面:
    • Create.cshtmlCreate.cshtml.cs
    • Delete.cshtmlDelete.cshtml.cs
    • Details.cshtmlDetails.cshtml.cs
    • Edit.cshtmlEdit.cshtml.cs
    • Index.cshtmlIndex.cshtml.cs
  • 创建 Data/SchoolContext.cs
  • Startup.cs 中为依赖注入添加上下文。
  • 将数据库连接字符串添加到 appsettings.json

数据库连接字符串

基架工具会在 appsettings.json 文件中生成连接字符串。

此连接字符串指定了 SQL Server LocalDB

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "SchoolContext": "Server=(localdb)\\mssqllocaldb;Database=CU-1;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

LocalDB 是轻型版本 SQL Server Express 数据库引擎,专门针对应用开发,而非生产使用。 默认情况下,LocalDB 会在 目录中创建 .mdf 文件。

更新数据库上下文类

数据库上下文类是为给定数据模型协调 EF Core 功能的主类。 上下文派生自 Microsoft.EntityFrameworkCore.DbContext。 上下文指定数据模型中包含哪些实体。 在此项目中,该类名为 SchoolContext

使用以下代码更新 Data/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<Student> Student 更改为复数形式的 DbSet<Student> Students。 若要使 Razor 页面代码与新的 DBSet 名称匹配,请从以下项进行全局更改:将 _context.Student. 更改为:_context.Students.

共出现 8 次。

由于一个实体集包含多个实体,因此许多开发人员更倾向于使用复数形式的 DBSet 属性名称。

突出显示的代码:

  • 将为每个实体集创建一个 DbSet<TEntity> 属性。 用 EF Core 的术语来说:
    • 实体集通常对应数据库表。
    • 一个实体对应于表中的一行。
  • 调用 OnModelCreatingOnModelCreating
    • SchoolContext 已初始化之后、但在模型受到保护并用于初始化上下文之前调用。
    • 是必需的,因为在本教程的后续部分中,Student 实体将引用其他实体。

生成项目以验证没有任何编译器错误。

Startup.cs

ASP.NET Core 通过依赖关系注入进行生成。 服务(例如 SchoolContext)在应用程序启动期间通过依赖关系注入进行注册。 需要这些服务(如 Razor 页面)的组件通过构造函数参数提供相应服务。 本教程的后续部分介绍了用于获取数据库上下文实例的构造函数代码。

基架工具自动将上下文类注册到了依赖项注入容器。

以下高亮显示的行由脚手架工具添加:

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    services.AddDbContext<SchoolContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("SchoolContext")));
}

通过在 DbContextOptions 对象上调用一个方法,将连接字符串的名称传入上下文。 进行本地开发时,ASP.NET Core 配置系统appsettings.json 文件中读取连接字符串。

添加数据库异常筛选器

添加 AddDatabaseDeveloperPageExceptionFilterUseMigrationsEndPoint,如下面的代码所示:

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();

    services.AddDbContext<SchoolContext>(options =>
       options.UseSqlServer(Configuration.GetConnectionString("SchoolContext")));

    services.AddDatabaseDeveloperPageExceptionFilter();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
        app.UseMigrationsEndPoint();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    app.UseRouting();

    app.UseAuthorization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapRazorPages();
    });
}

添加 Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore NuGet 包。

在包管理器控制台中,输入以下命令来添加 NuGet 包:

Install-Package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore

Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore NuGet 包提供用于 Entity Framework Core 错误页的 ASP.NET Core 中间件。 此中间件有助于检测和诊断 Entity Framework Core 迁移错误。

AddDatabaseDeveloperPageExceptionFilter开发环境中为 EF 迁移错误提供有用的错误信息。

创建数据库

如果没有数据库,请更新 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();
                    // 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>();
                });
    }
}

如果有上下文的数据库,则 EnsureCreated 方法不执行任何操作。 如果没有数据库,则它将创建数据库和架构。 EnsureCreated 启用以下工作流来处理数据模型更改:

  • 删除数据库。 任何现有数据丢失。
  • 更改数据模型。 例如,添加 EmailAddress 字段。
  • 运行应用。
  • EnsureCreated 创建具有新架构的数据库。

在无需保存数据的情况下,当架构快速发展时,此工作流在早期开发过程中表现良好。 如果需要保存已输入数据库的数据,情况就有所不同了。 在这种情况下,请使用迁移功能。

本系列教程的后续部分将删除 EnsureCreated 创建的数据库,转而使用迁移。 由 EnsureCreated 创建的数据库无法通过迁移进行更新。

测试应用

  • 运行应用。
  • 选择 学生 链接,然后选择 创建新项
  • 测试“编辑”、“详细信息”和“删除”链接。

填充数据库初始数据

EnsureCreated 方法将创建空数据库。 本节添加用测试数据填充数据库的代码。

使用以下代码创建 Data/DbInitializer.cs

using ContosoUniversity.Models;
using System;
using System.Linq;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            // 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")}
            };

            context.Students.AddRange(students);
            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}
            };

            context.Courses.AddRange(courses);
            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},
            };

            context.Enrollments.AddRange(enrollments);
            context.SaveChanges();
        }
    }
}

该代码会检查数据库中是否存在任何学生。 如果不存在学生,它将向数据库添加测试数据。 该代码使用数组创建测试数据而不是使用 List<T> 集合是为了优化性能。

  • Program.cs 中,从 // 行中删除 DbInitializer.Initialize

      context.Database.EnsureCreated();
      DbInitializer.Initialize(context);
    
  • 如果应用正在运行,则停止应用,然后在包管理器控制台 (PMC) 中运行以下命令:

    Drop-Database -Confirm
    
    
  • 使用 Y 进行响应,以删除数据库。

  • 重新启动应用。
  • 选择“学生”页以查看预填充数据。

查看数据库

  • 从 Visual Studio 中的“视图”菜单打开 SQL Server 对象资源管理器 (SSOX) 。
  • 在 SSOX 中,依次选择 “(localdb)\MSSQLLocalDB”>“数据库”>“SchoolContext-{GUID}”。 数据库名称由先前提供的上下文名称再加上一个短划线和一个 GUID 生成。
  • 展开 Tables 节点。
  • 右键单击 Student 表,然后单击“查看数据”,以查看创建的列和插入到表中的行 。
  • 右键单击 Student 表,然后单击 View Code,查看 Student 模型如何映射到 Student 表架构。

异步代码

异步编程是 ASP.NET Core 和 EF Core 的默认模式。

Web 服务器的可用线程是有限的,而在高负载情况下的可能所有线程都被占用。 当发生这种情况的时候,服务器就无法处理新请求,直到线程被释放。 使用同步代码时,可能会出现多个线程被占用但不能执行操作的情况,因为它们正在等待 I/O 完成。 使用异步代码时,当进程正在等待 I/O 完成,服务器可以将其线程释放用于处理其他请求。 因此,使用异步代码可以更有效地利用服务器资源,并且服务器可以无延迟地处理更多流量。

异步代码会在运行时引入少量开销。 流量较低时,对性能的影响可以忽略不计,但流量较高时,潜在的性能改善非常显著。

在以下代码中,async 关键字和 Task 返回值,await 关键字和 ToListAsync 方法让代码异步执行。

public async Task OnGetAsync()
{
    Students = await _context.Students.ToListAsync();
}
  • async 关键字让编译器执行以下操作:
    • 为方法体的各个部分生成回调。
    • 创建返回的 Task 对象。
  • 返回类型 Task 表示正在进行的工作。
  • await 关键字让编译器将该方法拆分为两个部分。 第一部分以异步启动的操作结束。 第二部分被放入一个在操作完成时调用的回调方法中。
  • ToListAsyncToList 扩展方法的异步版本。

编写使用 EF Core 的异步代码时需要注意的一些事项:

  • 只有导致查询或发送数据库命令的语句才能以异步方式执行。 这包括 ToListAsyncSingleOrDefaultAsyncFirstOrDefaultAsyncSaveChangesAsync。 不包括只会更改 IQueryable 的语句,例如 var students = context.Students.Where(s => s.LastName == "Davolio")
  • EF Core 上下文不是线程安全的:请勿尝试并行执行多个操作。
  • 若要利用异步代码的性能优势,请验证在调用向数据库发送查询的 EF Core 方法时,库程序包(例如用于分页)是否使用异步。

有关 .NET 中异步编程的详细信息,请参阅异步概述使用 Async 和 Await 的异步编程

性能注意事项

通常,网页不应加载任何数量的行。 查询应使用分页或限制结果数量的方式。 例如,上述查询可以使用 Take 来限制返回的行数:

public async Task OnGetAsync()
{
    Student = await _context.Students.Take(10).ToListAsync();
}

如果在枚举进行到一部分时发生数据库异常,那么在视图中枚举大型表可能会返回部分生成的 HTTP 200 响应。

MaxModelBindingCollectionSize 默认值为 1024。 以下代码将设置 MaxModelBindingCollectionSize

public void ConfigureServices(IServiceCollection services)
{
    var myMaxModelBindingCollectionSize = Convert.ToInt32(
                Configuration["MyMaxModelBindingCollectionSize"] ?? "100");

    services.Configure<MvcOptions>(options =>
           options.MaxModelBindingCollectionSize = myMaxModelBindingCollectionSize);

    services.AddRazorPages();

    services.AddDbContext<SchoolContext>(options =>
          options.UseSqlServer(Configuration.GetConnectionString("SchoolContext")));

    services.AddDatabaseDeveloperPageExceptionFilter();
}

有关配置设置(如 MyMaxModelBindingCollectionSize)的信息,请参阅配置

稍后将在教程中介绍分页。

有关详细信息,请参阅性能注意事项 (EF)

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 输出窗口中。

有关详细信息,请参阅以下资源:

后续步骤

将 SQLite 用于开发,将 SQL Server 用于生产

本文是系列教程的第一篇,这些教程展示如何在 ASP.NET Core Razor Pages 应用中使用 Entity Framework (EF) Core。 这些教程将为一所虚构的 Contoso University 构建一个网站。 网站包括学生录取、课程创建和讲师分配等功能。 本教程使用代码优先方法。 有关使用数据库优先方法学习本教程的信息,请参阅此 Github 问题

下载或查看已完成的应用。下载说明

Prerequisites

  • 如果不熟悉 Razor Pages,则在开始前,请浏览 Razor Pages 入门系列教程。

数据库引擎

Visual Studio 指令使用 SQL Server LocalDB,它是只在 Windows 上运行的一种 SQL Server Express 版本。

Visual Studio Code 指令使用 SQLite,一种跨平台数据库引擎。

如果选择使用 SQLite,请下载并安装适用于 SQLite 的数据库浏览器等第三方工具,用于管理和查看 SQLite 数据库。

Troubleshooting

如果遇到无法解决的问题,请将你的代码与完成的项目进行比较。 获取帮助的一个好方法是使用 ASP.NET Core 标记EF Core 标记将问题发布到 StackOverflow.com。

示例应用

这些教程中所构建的应用是一个基本的大学网站。 用户可以查看和更新学生、课程和讲师信息。 以下是在本教程中创建的几个屏幕。

“学生索引”页

学生编辑页

此网站的 UI 样式基于内置的项目模板。 本教程侧重于如何使用 EF Core,而不是如何自定义 UI。

单击页面顶部的链接,获取已完成项目的源代码。 “cu30”文件夹中有本教程的 ASP.NET Core 3.0 版本的代码。 在“cu30snapshots”文件夹中可以找到反映教程 1-7 代码状态的文件。

若要在下载完成的项目之后运行应用,请执行以下操作:

  • 构建项目。

  • 在包管理器控制台 (PMC) 中运行以下命令:

    Update-Database
    
  • 运行项目以初始化数据库数据。

创建 Web 应用项目

  • 从 Visual Studio“文件”菜单中选择“新建”“项目”>
  • 选择“ASP.NET Core Web 应用程序”。
  • 将该项目命名为 ContosoUniversity 。 请务必使用此名称(含大写),确保在复制和粘贴代码时与命名空间相匹配。
  • 在下拉列表中选择“.NET Core”和“ASP.NET Core 3.0”,然后选择“Web 应用程序” 。

设置网站样式

更新 Pages/Shared/_Layout.cshtml 以设置网站的页眉、页脚和菜单:

  • 将每一处出现的“ContosoUniversity”更改为“Contoso University”。 有三处。

  • 删除 HomePrivacy 菜单项,然后添加“关于”、“学生”、“课程”、“讲师”和“院系”的菜单项。

突出显示所作更改。

<!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">
            &copy; 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/main/aspnetcore/data/ef-rp/intro/samples" class="stretched-link">See project source code</a>
                </p>
            </div>
        </div>
    </div>
</div>

运行应用以验证主页是否显示。

数据模型

以下部分用于创建数据模型:

Course-Enrollment-Student 数据模型关系图

一名学生可以修读任意数量的课程,并且某一课程可以有任意数量的学生修读。

学生实体

学生实体图

  • 在项目文件夹中创建“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 将名为 IDclassnameID 的属性解释为主键。 因此,Student 类主键的另一种自动识别的名称是 StudentID。 有关详细信息,请参阅 EF Core - 键

Enrollments 属性是导航属性。 导航属性中包含与此实体相关的其他实体。 在本例中,Enrollments 实体的 Student 属性包含与该 Student 相关的所有 Enrollment 实体。 例如,如果数据库中的 Student 行有两个相关的 Enrollment 行,则 Enrollments 导航属性包含这两个 Enrollment 实体。

在数据库中,如果 StudentID 列包含学生的 ID 值,则 Enrollment 行与 Student 行相关。 例如,假设 Student 表中的一行的 ID 为 1。 相关注册行的 StudentID 将为 1。 StudentID 是 Enrollment 表中的 外键

Enrollments 属性被定义为 ICollection<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。 对于生产数据模型,请选择一个模式并一直使用。 本教程同时使用这两种方式,只是为了说明两者都可行。 使用不具有 IDclassname 可以更轻松地实现某些类型的数据模型更改。

Grade 属性为 enumGrade 类型声明后的问号(`?`)表示 Grade 属性可为 null。 值为 null 的分数与零分不同——null 表示该分数未知或尚未给出。

StudentID 属性是外键,其对应的导航属性为 StudentEnrollment 实体与一个 Student 实体相关联,因此该属性只包含一个 Student 实体。

CourseID 属性是外键,其对应的导航属性为 CourseEnrollment 实体与一个 Course 实体相关联。

如果属性命名为 EF Core,<navigation property name><primary key property name> 会将其视为外键。 例如,StudentIDStudent 导航属性的外键,因为 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 基架工具生成以下内容:

  • EF Corecontext 类。 上下文是为给定数据模型协调实体框架功能的主类。 它派生自 Microsoft.EntityFrameworkCore.DbContext 类。
  • Razor 页面,可处理 Student 实体的创建、读取、更新和删除 (CRUD) 操作。
  • 在“Pages”文件夹中创建“Students”文件夹 。
  • “解决方案资源管理器”中,右键单击“Pages/Students”文件夹,然后选择“添加”>“新建基架项”。
  • 添加基架 对话框中,选择 Razor使用 Entity Framework 的页面(CRUD)>和 添加
  • “添加Razor使用 Entity Framework 的页面 (CRUD)”对话框中:
    • 在“模型类”下拉列表中,选择“Student (ContosoUniversity.Models)” 。
    • 在“数据上下文类”行中,选择 +(加号)。
    • 将数据上下文名称从 ContosoUniversity.Models.ContosoUniversityContext 更改为 ContosoUniversity.Data.SchoolContext 。
    • 选择 并添加

自动安装以下包:

  • Microsoft.VisualStudio.Web.CodeGeneration.Design
  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.Extensions.Logging.Debug
  • Microsoft.EntityFrameworkCore.Tools

如果对上述步骤有疑问,请生成项目并重试基架搭建步骤。

基架流程:

  • 在“Pages/Students”Razor文件夹中创建 页面:
    • Create.cshtmlCreate.cshtml.cs
    • Delete.cshtmlDelete.cshtml.cs
    • Details.cshtmlDetails.cshtml.cs
    • Edit.cshtmlEdit.cshtml.cs
    • Index.cshtmlIndex.cshtml.cs
  • 创建 Data/SchoolContext.cs
  • Startup.cs 中为依赖注入添加上下文。
  • 将数据库连接字符串添加到 appsettings.json

数据库连接字符串

appsettings.json 文件指定了连接字符串 SQL Server LocalDB

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "SchoolContext": "Server=(localdb)\\mssqllocaldb;Database=SchoolContext6;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

LocalDB 是轻型版本 SQL Server Express 数据库引擎,专门针对应用开发,而非生产使用。 默认情况下,LocalDB 会在 目录中创建 .mdf 文件。

更新数据库上下文类

数据库上下文类是为给定数据模型协调 EF Core 功能的主类。 上下文派生自 Microsoft.EntityFrameworkCore.DbContext。 上下文指定数据模型中包含哪些实体。 在此项目中,该类名为 SchoolContext

使用以下代码更新 Data/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 次。

生成项目以验证没有任何编译器错误。

Startup.cs

ASP.NET Core 通过依赖关系注入进行生成。 在应用程序启动过程中通过依赖关系注入注册相关服务(例如 EF Core 数据库上下文)。 需要这些服务(如 Razor 页面)的组件通过构造函数参数提供相应服务。 本教程的后续部分介绍了用于获取数据库上下文实例的构造函数代码。

基架工具自动将上下文类注册到了依赖项注入容器。

  • ConfigureServices 中,高亮显示的行是由脚手架工具添加的:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddRazorPages();
    
        services.AddDbContext<SchoolContext>(options =>
                options.UseSqlServer(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();
                    // 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>();
                });
    }
}

如果有上下文的数据库,则 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")}
            };

            context.Students.AddRange(students);
            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}
            };

            context.Courses.AddRange(courses);
            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},
            };

            context.Enrollments.AddRange(enrollments);
            context.SaveChanges();
        }
    }
}

该代码会检查数据库中是否存在任何学生。 如果不存在学生,它将向数据库添加测试数据。 该代码使用数组创建测试数据而不是使用 List<T> 集合是为了优化性能。

  • Program.cs 中,将 EnsureCreated 调用替换为 DbInitializer.Initialize 调用:

    // context.Database.EnsureCreated();
    DbInitializer.Initialize(context);
    

如果应用正在运行,则停止应用,然后在包管理器控制台 (PMC) 中运行以下命令:

Drop-Database
  • 重新启动应用。

  • 选择“学生”页以查看预填充数据。

查看数据库

  • 从 Visual Studio 中的“视图”菜单打开 SQL Server 对象资源管理器 (SSOX) 。
  • 在 SSOX 中,依次选择 “(localdb)\MSSQLLocalDB”>“数据库”>“SchoolContext-{GUID}”。 数据库名称由你之前提供的上下文名称再加上一个短划线和一个 GUID 生成。
  • 展开 Tables 节点。
  • 右键单击 Student 表,然后单击“查看数据”,以查看创建的列和插入到表中的行 。
  • 右键单击 Student 表,然后单击 View Code,查看 Student 模型如何映射到 Student 表架构。

异步代码

异步编程是 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 对象。
  • 返回类型 Task<T> 表示正在进行的工作。
  • await 关键字让编译器将该方法拆分为两个部分。 第一部分以异步启动的操作结束。 第二部分被放入一个在操作完成时调用的回调方法中。
  • ToListAsyncToList 扩展方法的异步版本。

编写使用 EF Core 的异步代码时需要注意的一些事项:

  • 只有导致查询或发送数据库命令的语句才能以异步方式执行。 这包括 ToListAsyncSingleOrDefaultAsyncFirstOrDefaultAsyncSaveChangesAsync。 不包括只会更改 IQueryable 的语句,例如 var students = context.Students.Where(s => s.LastName == "Davolio")
  • EF Core 上下文不是线程安全的:请勿尝试并行执行多个操作。
  • 若要利用异步代码的性能优势,请验证在调用向数据库发送查询的 EF Core 方法时,库程序包(例如用于分页)是否使用异步。

有关 .NET 中异步编程的详细信息,请参阅异步概述使用 Async 和 Await 的异步编程

后续步骤