教程:在 ASP.NET MVC Web 应用中使用 EF Core 入门

作者:Tom DykstraRick Anderson

本教程介绍具有控制器和视图的 ASP.NET Core MVC 和 Entity Framework Core。 Razor Pages 是一种备用编程模型。 对于新的开发,我们建议在具有控制器和视图的 MVC 上使用 Razor Pages。 请参阅本教程的 Razor Pages 版本。 每个教程涵盖其他教程没有的一些资料:

此 MVC 教程中包含但 Razor Pages 教程中没有的某些内容:

  • 在数据模型中实现继承
  • 执行原始 SQL 查询
  • 使用动态 LINQ 简化代码

Razor Pages 教程中包含但此 MVC 教程中没有的某些内容:

  • 使用 Select 方法加载相关数据
  • EF 最佳做法。

Contoso University 示例 Web 应用演示了如何使用 Entity Framework (EF) Core 和 Visual Studio 创建 ASP.NET Core MVC Web 应用。

该示例应用是一个虚构的 Contoso University 的网站。 其中包括学生录取、课程创建和讲师分配等功能。 这是介绍如何构建 Contoso University 示例应用系列教程中的第一部分。

先决条件

本教程没有针对 ASP.NET Core 6 或更高版本进行更新。 如果创建面向 ASP.NET Core 6 或更高版本的项目,本教程的说明将不适用。 例如,ASP.NET Core 6 和更高版本 Web 模板使用了最小托管模型,它将 Startup.csProgram.cs 统一到单个 Program.cs 文件中。

.NET 6 中引入的另一个区别是 NRT(可为空引用类型)功能。 项目模板默认启用此功能。 如果 EF 认为某个属性在 .NET 6 中是必需的,而在 .NET 5 中可为空,则可能会出现问题。 例如,除非将 Enrollments 属性设置为可为空或将 asp-validation-summary 帮助程序标记从 ModelOnly 更改为 All,否则“创建学生”页面将失败且无提示。

对于本教程,建议安装并使用 .NET 5 SDK。 在更新本教程之前,请参阅 ASP.NET Core 中的 Razor Pages 和 Entity Framework Core - 教程 1(共 8 个),了解如何将 Entity Framework 与 ASP.NET Core 6 或更高版本配合使用。

数据库引擎

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

解决问题和进行故障排除

如果遇到无法解决的问题,可以通过与 已完成的项目对比代码来查找解决方案。 常见错误以及对应的解决方案,请参阅 最新教程中的故障排除。 如果没有找到遇到的问题的解决方案,可以将问题发布到StackOverflow.com 的 ASP.NET CoreEF Core 版块。

提示

这是一系列一共有十个教程,其中每个都是在前面教程已完成的基础上继续。 请考虑在完成每一个教程后保存项目的副本。 之后如果遇到问题,你可以从保存的副本中开始寻找问题,而不是从头开始。

Contoso University Web 应用

这些教程中所构建的应用是一个基本的大学网站。

用户可以查看和更新学生、课程和讲师信息。 以下是该应用中的几个屏幕:

“学生索引”页

学生编辑页

创建 Web 应用

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

设置网站样式

设置网站菜单、布局和 home 页时需作少量基础更改。

打开 Views/Shared/_Layout.cshtml 并进行下列更改:

  • 将每次出现的 ContosoUniversity 更改为 Contoso University。 需要更改三个地方。
  • 添加“关于”、“学生”、“课程”、“讲师”和“院系”的菜单项,并删除“Privacy”菜单项。

前面的更改在以下代码中突出显示:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - Contoso University</title>
    <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Contoso University</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="About">About</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Students" asp-action="Index">Students</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Courses" asp-action="Index">Courses</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Instructors" asp-action="Index">Instructors</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Departments" asp-action="Index">Departments</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2020 - Contoso University - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>
    <script src="~/lib/jquery/dist/jquery.js"></script>
    <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
    <script src="~/js/site.js" asp-append-version="true"></script>
    @await RenderSectionAsync("Scripts", required: false)
</body>
</html>

Views/Home/Index.cshtml 中,将文件的内容替换为以下标记:

@{
    ViewData["Title"] = "Home Page";
}

<div class="jumbotron">
    <h1>Contoso University</h1>
</div>
<div class="row">
    <div class="col-md-4">
        <h2>Welcome to Contoso University</h2>
        <p>
            Contoso University is a sample application that
            demonstrates how to use Entity Framework Core in an
            ASP.NET Core MVC web application.
        </p>
    </div>
    <div class="col-md-4">
        <h2>Build it from scratch</h2>
        <p>You can build the application by following the steps in a series of tutorials.</p>
        <p><a class="btn btn-default" href="https://docs.asp.net/en/latest/data/ef-mvc/intro.html">See the tutorial &raquo;</a></p>
    </div>
    <div class="col-md-4">
        <h2>Download it</h2>
        <p>You can download the completed project from GitHub.</p>
        <p><a class="btn btn-default" href="https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-mvc/intro/samples/5cu-final">See project source code &raquo;</a></p>
    </div>
</div>

按 CTRL+F5 来运行该项目,或从菜单选择“调试”>“开始执行(不调试)”。 home 页将显示与本教程中创建的页面对应的选项卡。

Contoso University home 页

EF Core NuGet 包

本教程使用 SQL Server,相关驱动包Microsoft.EntityFrameworkCore.SqlServer

EF SQL Server 包与其依赖项 Microsoft.EntityFrameworkCoreMicrosoft.EntityFrameworkCore.Relational 一起提供 EF 的运行时支持。

添加 Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore NuGet 包。 在包管理器控制台 (PMC) 中,输入以下命令来添加 NuGet 包:

Install-Package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
Install-Package Microsoft.EntityFrameworkCore.SqlServer

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

有关其他可用于 EF Core 的数据库提供程序的信息,请参阅数据库提供程序

创建数据模型

将为此应用创建以下实体类:

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

前面的实体具有以下关系:

  • StudentEnrollment 实体之间存在一对多关系。 一名学生可以报名参加任意数量的课程。
  • CourseEnrollment 实体之间存在一对多关系。 一门课程中可以包含任意数量的学生。

以下部分将为这几个实体中的每一个实体创建一个类。

Student 实体

Student 实体关系图

在 Models 文件夹中,使用以下代码创建 Student 类:

using System;
using System.Collections.Generic;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

ID 属性是对应于此类的数据库表的主键 (PK) 列。 默认情况下,EF 将名为 IDclassnameID 的属性解释为主键。 例如,可以将 PK 命名为 StudentID 而不是 ID

Enrollments 属性是导航属性。 导航属性中包含与此实体相关的其他实体。 Student 实体的 Enrollments 属性:

  • 包含与该 Student 实体相关的所有 Enrollment 实体。
  • 如果数据库中的特定 Student 行具有两个相关的 Enrollment 行:
    • Student 实体的 Enrollments 导航属性包含这两个 Enrollment 实体。

Enrollment 行在 StudentID 外键 (FK) 列中包含学生的 PK 值。

如果导航属性可以包含多个实体:

  • 类型必须为列表,如 ICollection<T>List<T>HashSet<T>
  • 可以添加、删除和更新实体。

多对多和一对多导航关系可包含多个实体。 使用 ICollection<T> 时,EF 会默认创建 HashSet<T> 集合。

Enrollment 实体

Enrollment 实体关系图

在 Models 文件夹中,使用以下代码创建 Enrollment 类:

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        public Grade? Grade { get; set; }

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

EnrollmentID 属性为 PK。 该实体使用 ID 模式,而不是本身使用的 classnameID 模式。 Student 实体使用 ID 模式。 某些开发人员更喜欢在整个数据模型中使用一种模式。 在本教程中,这种变化说明了两种模式都可以使用。 后面的教程介绍,使用不包含 classname 的 ID 可以更轻松地在数据模型中实现继承。

Grade 属性为 enumGrade 类型声明后的 ? 表示 Grade 属性可以为 nullnull 级别与零级别不同。 null 表示级别未知或尚未分配。

StudentID 属性是外键 (FK),其对应的导航属性为 StudentEnrollment 实体与一个 Student 实体相关联,因此该属性只能包含一个 Student 实体。 这与 Student.Enrollments 导航属性不同,后者可包含多个 Enrollment 实体。

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

如果将属性命名为<导航属性名称><主键属性名称>,则 Entity Framework 将该属性解释为 FK 属性。 例如 Student 导航属性的 StudentID,因为 Student 实体的 PK 为 ID。 还可以将 FK 属性命名为 <主键属性名称>。 例如 CourseID,因为 Course 实体的 PK 为 CourseID

Course 实体

Course 实体关系图

在 Models 文件夹中,使用以下代码创建 Course 类:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int CourseID { get; set; }
        public string Title { get; set; }
        public int Credits { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

Enrollments 属性是导航属性。 Course 实体可与任意数量的 Enrollment 实体相关。

DatabaseGenerated 属性将在后面的教程部分中介绍。 该属性允许为课程输入 PK,而不是让数据库生成 PK。

创建数据库上下文

DbContext 数据库上下文类是为给定数据模型协调 EF 功能的主类。 此类由 Microsoft.EntityFrameworkCore.DbContext 类派生而来。 DbContext 派生类指定数据模型中包含哪些实体。 某些 EF 行为可自定义。 在此项目中将数据库上下文类命名为 SchoolContext

在项目文件夹中,创建名为的文件夹 Data

在 Data 文件夹中,使用以下代码创建 SchoolContext 类:

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
    }
}

前面的代码为每个实体集创建 DbSet 属性。 在 EF 术语中:

  • 实体集通常对应数据库表。
  • 实体对应表中的行。

可以省略 DbSet<Enrollment>DbSet<Course> 语句,实现的功能没有任何改变。 EF 将隐式包含它们,原因如下:

  • Student 实体引用 Enrollment 实体。
  • Enrollment 实体引用 Course 实体。

当数据库创建完成后, EF 创建一系列数据表,表名默认和 DbSet 属性名相同。 集合的属性名通常采用复数形式。 例如,使用 Students,而不使用 Student。 开发者对表名称是否应为复数意见不一。 在这些教程中,在 DbContext 中指定单数形式的表名称会覆盖默认行为。 此教程在最后一个 DbSet 属性之后,添加以下高亮显示的代码。

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
        }
    }
}

注册 SchoolContext

ASP.NET Core 包含依赖关系注入。 在应用启动过程中通过依赖项注入来注册相关服务,例如 EF 数据库上下文。 需要这些服务的组件(如 MVC 控制器)可以通过构造函数参数来获得这些服务。 本教程后面部分将展示获取上下文实例的控制器构造函数代码。

若要将 SchoolContext 注册为一种服务,打开 Startup.cs,并将突出显示的代码行添加到 ConfigureServices 方法中。

using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace ContosoUniversity
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<SchoolContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            services.AddControllersWithViews();
        }

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

打开 appsettings.json 文件,并按以下标记所示添加连接字符串:

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

添加数据库异常筛选器

AddDatabaseDeveloperPageExceptionFilter 添加到 ConfigureServices,如下面的代码所示:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<SchoolContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

    services.AddDatabaseDeveloperPageExceptionFilter();

    services.AddControllersWithViews();
}

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

SQL Server Express LocalDB

连接字符串指定 SQL Server LocalDB。 LocalDB 是轻型版本 SQL Server Express 数据库引擎,专门针对应用开发,而非生产使用。 LocalDB 作为按需启动并在用户模式下运行的轻量级数据库没有复杂的配置。 默认情况下,LocalDB 会在 C:/Users/<user> 目录中创建 .mdf 数据库文件。

使用测试数据初始化数据库

EF 将创建空数据库。 在本部分中,你将添加一个方法,该方法将在数据库创建后调用,以向数据库填充测试数据。

EnsureCreated 方法用于自动创建数据库。 在后面的教程中,你将了解如何处理模型更改,方法是使用 Code First 迁移来更改数据库架构,而不是删除和重新创建数据库。

在 Data 文件夹中,使用以下代码创建一个名为 DbInitializer 的新类:

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

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            context.Database.EnsureCreated();

            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
            new Student{FirstMidName="Carson",LastName="Alexander",EnrollmentDate=DateTime.Parse("2005-09-01")},
            new Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Parse("2002-09-01")},
            new Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse("2003-09-01")},
            new Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Parse("2002-09-01")},
            new Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2002-09-01")},
            new Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Parse("2001-09-01")},
            new Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse("2003-09-01")},
            new Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Parse("2005-09-01")}
            };
            foreach (Student s in students)
            {
                context.Students.Add(s);
            }
            context.SaveChanges();

            var courses = new Course[]
            {
            new Course{CourseID=1050,Title="Chemistry",Credits=3},
            new Course{CourseID=4022,Title="Microeconomics",Credits=3},
            new Course{CourseID=4041,Title="Macroeconomics",Credits=3},
            new Course{CourseID=1045,Title="Calculus",Credits=4},
            new Course{CourseID=3141,Title="Trigonometry",Credits=4},
            new Course{CourseID=2021,Title="Composition",Credits=3},
            new Course{CourseID=2042,Title="Literature",Credits=4}
            };
            foreach (Course c in courses)
            {
                context.Courses.Add(c);
            }
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
            new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
            new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
            new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
            new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
            new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
            new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
            new Enrollment{StudentID=3,CourseID=1050},
            new Enrollment{StudentID=4,CourseID=1050},
            new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
            new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
            new Enrollment{StudentID=6,CourseID=1045},
            new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
            };
            foreach (Enrollment e in enrollments)
            {
                context.Enrollments.Add(e);
            }
            context.SaveChanges();
        }
    }
}

前面的代码检查数据库是否存在:

  • 如果找不到数据库;
    • 则创建一个数据库并使用测试数据加载。 代码中使用数组存放测试数据而不是使用 List<T> 集合是为了优化性能。
  • 如果找到数据库,则不执行任何操作。

使用以下代码更新 Program.cs

using ContosoUniversity.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;

namespace ContosoUniversity
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            CreateDbIfNotExists(host);

            host.Run();
        }

        private static void CreateDbIfNotExists(IHost host)
        {
            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;
                try
                {
                    var context = services.GetRequiredService<SchoolContext>();
                    DbInitializer.Initialize(context);
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred creating the DB.");
                }
            }
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

Program.cs 在应用启动时执行以下操作:

  • 从依赖注入容器中获取数据库上下文实例。
  • 调用 DbInitializer.Initialize 方法。
  • Initialize 方法完成时释放上下文,如以下代码所示:
public static void Main(string[] args)
{
     var host = CreateWebHostBuilder(args).Build();

    using (var scope = host.Services.CreateScope())
    {
        var services = scope.ServiceProvider;
        try
        {
            var context = services.GetRequiredService<SchoolContext>();
            DbInitializer.Initialize(context);
        }
        catch (Exception ex)
        {
            var logger = services.GetRequiredService<ILogger<Program>>();
            logger.LogError(ex, "An error occurred while seeding the database.");
        }
    }

    host.Run();
}

第一次运行该应用时,会创建数据库并使用测试数据加载该数据库。 每当数据模型发生更改时:

  • 删除数据库。
  • 更新 Seed 方法,并使用新数据库重新开始。

在后面的教程中,在更改数据模型时会修改数据库,而不会删除和重新创建数据库。 数据模型发生更改时不会丢失任何数据。

创建控制器和视图

使用 Visual Studio 中的基架引擎添加一个 MVC 控制器,以及使用 EF 来查询和保存数据的视图。

CRUD 操作方法和视图的自动创建被称为基架。

  • 在“解决方案资源管理器”中,右键单击 Controllers 文件夹,然后选择“添加”>“新搭建基架的项目”。
  • 在“添加基架”对话框中
    • 选择 视图使用 Entity Framework 的 MVC 控制器
    • 单击 添加。 随即将显示“使用实体框架添加包含视图的 MVC 控制器”对话框:搭建“学生”基架
    • 在“模型类”中选择“Student”。
    • 在“数据上下文类”中选择“SchoolContext” 。
    • 使用 StudentsController 作为默认名称。
    • 单击 添加

Visual Studio 基架引擎创建 StudentsController.cs 文件和一组对应于控制器的视图(*.cshtml 文件)。

注意控制器将 SchoolContext 作为构造函数参数。

namespace ContosoUniversity.Controllers
{
    public class StudentsController : Controller
    {
        private readonly SchoolContext _context;

        public StudentsController(SchoolContext context)
        {
            _context = context;
        }

ASP.NET Core 依赖关系注入负责将 SchoolContext 实例传递到控制器。 你在 Startup 类中对其进行配置。

控制器包含 Index 操作方法,用于显示数据库中的所有学生。 该方法从学生实体集中获取学生列表,学生实体集则是通过读取数据库上下文实例中的 Students 属性获得:

public async Task<IActionResult> Index()
{
    return View(await _context.Students.ToListAsync());
}

本教程后面部分将介绍此代码中的异步编程元素。

Views/Students/Index.cshtml 视图使用table标签显示此列表:

@model IEnumerable<ContosoUniversity.Models.Student>

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                <th>
                    @Html.DisplayNameFor(model => model.LastName)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.FirstMidName)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.EnrollmentDate)
                </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.LastName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.FirstMidName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

按 CTRL+F5 来运行该项目,或从菜单选择“调试”>“开始执行(不调试)”。

单击学生选项卡以查看 DbInitializer.Initialize 插入的测试的数据。 你将看到 Students 选项卡链接在页的顶部或在单击右上角后的导航图标中,具体显示在哪里取决于浏览器窗口宽度。

Contoso University home 页窄

“学生索引”页

查看数据库

启动应用时,DbInitializer.Initialize 方法会调用 EnsureCreated。 EF 看到没有数据库:

  • 因此,它创建了一个数据库。
  • Initialize 方法代码用数据填充数据库。

在 Visual Studio 中使用“SQL Server 对象资源管理器”(SSOX) 查看数据库:

  • 从 Visual Studio 中的“视图”菜单选择“SQL Server 对象资源管理器”。
  • 在 SSOX 中,选择“(localdb)\MSSQLLocalDB”>“数据库”。
  • 选择 appsettings.json 文件中的连接字符串中的数据库名称的实体 ContosoUniversity1
  • 展开“表”节点,查看数据库中的表。

SSOX 中的表

右键单击“Student”表,然后单击“查看数据”以查看表中的数据。

SSOX 中的 Student 表

*.mdf*.ldf 数据库文件位于“C:\Users\<username>”文件夹中。

由于 EnsureCreated 是在应用启动时运行的初始化方法中调用的,因此可以执行以下操作:

  • Student 类进行更改。
  • 删除数据库。
  • 停止,然后启动应用。 将自动重新创建数据库以匹配此更改。

例如,如果向 Student 类添加 EmailAddress 属性,则重新创建的表中会有新的 EmailAddress 列。 视图不会显示新的 EmailAddress 属性。

约定

由于使用了 EF 使用的约定,因此为使 EF 创建完整数据库而编写的代码量是最小的:

  • DbSet 类型的属性用作表名。 如果实体未被 DbSet 属性引用,实体类名称用作表名称。
  • 使用实体属性名作为列名。
  • 命名为 IDclassnameID 的实体属性识别为 PK 属性。
  • 如果将属性命名为<导航属性名称><PK 属性名称>,则该属性解释为 FK 属性。 例如 Student 导航属性的 StudentID,因为 Student 实体的 PK 为 ID。 还可以将 FK 属性命名为 <主键属性名称>。 例如 EnrollmentID,因为 Enrollment 实体的 PK 为 EnrollmentID

约定行为可以重写。 例如,可以显式指定表名,如本教程中前面部分所示。 列名称和任何属性都可以设置为 PK 或 FK。

异步代码

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

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

异步代码在运行时,会引入的少量开销,在低流量时对性能的影响可以忽略不计,但在针对高流量情况下潜在的性能提升是可观的。

在下面的代码中,asyncTask<T>awaitToListAsync 使代码以异步方式执行。

public async Task<IActionResult> Index()
{
    return View(await _context.Students.ToListAsync());
}
  • async 关键字用于告知编译器该方法主体将生成回调并自动创建 Task<IActionResult> 返回对象。
  • 返回类型 Task<IActionResult> 表示正在进行的工作返回的结果为 IActionResult 类型。
  • await 关键字会使得编译器将方法拆分为两个部分。 第一部分是以异步方式结束已启动的操作。 第二部分是当操作完成时注入调用回调方法的地方。
  • ToListAsyncToList 方法的的异步扩展版本。

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

  • 只有导致查询或发送数据库命令的语句才能以异步方式执行。 包括 ToListAsyncSingleOrDefaultAsync,和 SaveChangesAsync。 不包括只需更改 IQueryable 的语句,如 var students = context.Students.Where(s => s.LastName == "Davolio")
  • EF 上下文是线程不安全的: 请勿尝试并行执行多个操作。 当调用异步 EF 方法时,始终使用 await 关键字。
  • 若要利用异步代码的性能优势,请确保所使用的任何库包在它们调用导致发送至数据库的查询的任何 EF 方法时也使用异步。

有关在 .NET 异步编程的详细信息,请参阅 异步概述

限制提取的实体数

若要了解如何限制从查询返回的实体数,请参阅性能注意事项

Entity Framework Core 的 SQL 日志记录

日志配置通常由 appsettings.{Environment}.json 文件的 Logging 部分提供。 若要记录 SQL 语句,请将 "Microsoft.EntityFrameworkCore.Database.Command": "Information" 添加到 appsettings.Development.json 文件:

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=MyDB-2;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
     ,"Microsoft.EntityFrameworkCore.Database.Command": "Information"
    }
  },
  "AllowedHosts": "*"
}

有了前面的 JSON,SQL 语句就会显示在命令行和 Visual Studio 输出窗口中。

有关详细信息,请参阅 .NET Core 和 ASP.NET Core 中的日志记录和此 GitHub 问题

请继续阅读下一篇教程,了解如何执行基本的 CRUD(创建、读取、更新、删除)操作。

本教程介绍具有控制器和视图的 ASP.NET Core MVC 和 Entity Framework Core。 Razor Pages 是一种备用编程模型。 对于新的开发,我们建议在具有控制器和视图的 MVC 上使用 Razor Pages。 请参阅本教程的 Razor Pages 版本。 每个教程涵盖其他教程没有的一些资料:

此 MVC 教程中包含但 Razor Pages 教程中没有的某些内容:

  • 在数据模型中实现继承
  • 执行原始 SQL 查询
  • 使用动态 LINQ 简化代码

Razor Pages 教程中包含但此 MVC 教程中没有的某些内容:

  • 使用 Select 方法加载相关数据
  • EF 最佳做法。

Contoso University 示例 Web 应用程序演示如何使用 Entity Framework (EF) Core 2.2 和 Visual Studio 2019 创建 ASP.NET Core 2.2 MVC Web 应用程序。

本教程没有针对 ASP.NET Core 3.1 进行更新。 它已针对 ASP.NET Core 5.0 进行了更新。

示例应用程序供一个虚构的 Contoso 大学网站使用。 它包括诸如学生入学、 课程创建和导师分配等功能。 这是一系列教程中的第一个,这一系列教程主要展示了如何从零开始构建 Contoso 大学示例应用程序。

先决条件

疑难解答

如果遇到无法解决的问题,可以通过与 已完成的项目对比代码来查找解决方案。 常见错误以及对应的解决方案,请参阅 最新教程中的故障排除。 如果没有找到遇到的问题的解决方案,可以将问题发布到StackOverflow.com 的 ASP.NET CoreEF Core 版块。

提示

这是一系列一共有十个教程,其中每个都是在前面教程已完成的基础上继续。 请考虑在完成每一个教程后保存项目的副本。 之后如果遇到问题,你可以从保存的副本中开始寻找问题,而不是从头开始。

Contoso University Web 应用

你将在这些教程中学习构建一个简单的大学网站的应用程序。

用户可以查看和更新学生、 课程和教师信息。 以下是一些你即将创建的页面。

“学生索引”页

学生编辑页

创建 Web 应用

  • 打开 Visual Studio。

  • 从“文件”菜单中选择“新建”>“项目” 。

  • 从左窗格中依次选择“已安装”>“Visual C#”>“Web”。

  • 选择“ASP.NET Core Web 应用程序”项目模板。

  • 输入“ContosoUniversity”作为名称,然后单击“确定” 。

    “新建项目”对话框

  • 等待“新建 ASP.NET Core Web 应用程序”对话框显示出来。

  • 选择“.NET Core”、“ASP.NET Core 2.2”和“Web 应用程序(模型-视图-控制器)”模板 。

  • 请确保 身份验证 设置为 不进行身份验证

  • 选择“确定”

    新的 ASP.NET Core 项目对话框

设置网站样式

通过几个简单的更改设置站点菜单、 布局和 home 页。

打开 Views/Shared/_Layout.cshtml 并进行下列更改:

  • 将文件中的"ContosoUniversity"更改为"Contoso University"。 需要更改三个地方。

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

突出显示所作更改。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - Contoso University</title>

    <environment include="Development">
        <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
    </environment>
    <environment exclude="Development">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/css/bootstrap.css"
              asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.css"
              asp-fallback-test-class="sr-only" asp-fallback-test-property="position" asp-fallback-test-value="absolute"
              crossorigin="anonymous"
              integrity="sha256-eSi1q2PG6J7g7ib17yAaWMcrr5GrtohYChqibrV7PBE="/>
    </environment>
    <link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
    <header>
        <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
            <div class="container">
                <a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Contoso University</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                        aria-expanded="false" aria-label="Toggle navigation">
                    <span class="navbar-toggler-icon"></span>
                </button>
                <div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
                    <ul class="navbar-nav flex-grow-1">
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="About">About</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Students" asp-action="Index">Students</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Courses" asp-action="Index">Courses</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Instructors" asp-action="Index">Instructors</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link text-dark" asp-area="" asp-controller="Departments" asp-action="Index">Departments</a>
                        </li>
                    </ul>
                </div>
            </div>
        </nav>
    </header>
    <div class="container">
        <partial name="_CookieConsentPartial" />
        <main role="main" class="pb-3">
            @RenderBody()
        </main>
    </div>

    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2019 - Contoso University - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
        </div>
    </footer>

    <environment include="Development">
        <script src="~/lib/jquery/dist/jquery.js"></script>
        <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"></script>
    </environment>
    <environment exclude="Development">
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.js"
                asp-fallback-src="~/lib/jquery/dist/jquery.js"
                asp-fallback-test="window.jQuery"
                crossorigin="anonymous"
                integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=">
        </script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.1.3/js/bootstrap.bundle.js"
                asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.bundle.js"
                asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
                crossorigin="anonymous"
                integrity="sha256-E/V4cWE4qvAeO5MOhjtGtqDzPndRO1LBk8lJ/PR7CA4=">
        </script>
    </environment>
    <script src="~/js/site.js" asp-append-version="true"></script>

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

Views/Home/Index.cshtml 中,将文件的内容替换为以下代码以将有关 ASP.NET 和 MVC 的内容替换为有关此应用程序的内容:

@{
    ViewData["Title"] = "Home Page";
}

<div class="jumbotron">
    <h1>Contoso University</h1>
</div>
<div class="row">
    <div class="col-md-4">
        <h2>Welcome to Contoso University</h2>
        <p>
            Contoso University is a sample application that
            demonstrates how to use Entity Framework Core in an
            ASP.NET Core MVC web application.
        </p>
    </div>
    <div class="col-md-4">
        <h2>Build it from scratch</h2>
        <p>You can build the application by following the steps in a series of tutorials.</p>
        <p><a class="btn btn-default" href="https://docs.asp.net/en/latest/data/ef-mvc/intro.html">See the tutorial &raquo;</a></p>
    </div>
    <div class="col-md-4">
        <h2>Download it</h2>
        <p>You can download the completed project from GitHub.</p>
        <p><a class="btn btn-default" href="https://github.com/dotnet/AspNetCore.Docs/tree/main/aspnetcore/data/ef-mvc/intro/samples/cu-final">See project source code &raquo;</a></p>
    </div>
</div>

按 CTRL+F5 来运行该项目,或从菜单选择“调试”>“开始执行(不调试)”。 你会看到 home 页,以及通过这个教程创建的页对应的选项卡。

Contoso University home 页

关于 EF Core NuGet 包

若要为项目添加 EF Core 支持,请安装相应的数据提供程序。 本教程使用 SQL Server,相关驱动包Microsoft.EntityFrameworkCore.SqlServer。 此包包含在 Microsoft.AspNetCore.App 元包中,因此无需引用该包。

EF SQL Server 包和其依赖项(Microsoft.EntityFrameworkCoreMicrosoft.EntityFrameworkCore.Relational)一起提供 EF 的运行时支持。 你将在之后的 迁移 教程中学习添加工具包。

有关其他可用于 EF Core 的数据库驱动的信息,请参阅 数据库驱动

创建数据模型

接下来你将创建 Contoso 大学应用程序的实体类。 你将从以下三个实体类开始。

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

StudentEnrollment实体之间是一对多的关系,CourseEnrollment 实体之间也是一个对多的关系。 换而言之,一名学生可以修读任意数量的课程, 并且某一课程可以被任意数量的学生修读。

接下来,你将创建与这些实体对应的类。

Student 实体

Student 实体关系图

Models 文件夹中,创建一个名为 Student.cs 的类文件并且将模板代码替换为以下代码。

using System;
using System.Collections.Generic;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

ID 属性将成为对应于此类的数据库表中的主键。 默认情况下,EF 将会将名为 IDclassnameID 的属性解析为主键。

Enrollments 属性是导航属性。 导航属性中包含与此实体相关的其他实体。 在这个案例下,Student entity 中的 Enrollments 属性会保留所有与 Student 实体相关的 Enrollment。 换而言之,如果数据库中的 Student 行具有两个相关的 Enrollment 行(在其 StudentID 外键列中包含该学生的主键值的行),则该 Student 实体的 Enrollments 导航属性将包含这两个 Enrollment 实体。

如果导航属性可以具有多个实体 (如多对多或一对多关系),那么导航属性的类型必须是可以添加、 删除和更新条目的容器,如 ICollection<T>。 你可以指定 ICollection<T> 或实现该接口类型,如 List<T>HashSet<T>。 如果指定 ICollection<T>,EF在默认情况下创建 HashSet<T> 集合。

Enrollment 实体

修读实体关系图

Models 文件夹中,创建 Enrollment.cs 并且用以下代码替换现有代码:

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        public Grade? Grade { get; set; }

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

EnrollmentID 属性将被设为主键; 此实体使用 classnameID 模式而不是如 Student 实体那样直接使用 ID。 通常情况下,你选择一个主键模式,并在你的数据模型自始至终使用这种模式。 在这里,使用了两种不同的模式只是为了说明你可以使用任一模式来指定主键。 在 后面的教程,你将了解到使用`ID`这种模式可以更轻松地在数据模型之间实现继承。

Grade 属性是 enumGrade 声明类型后的`?`表示 Grade 属性可以为 null。 评级为 null 和评级为零是有区别的 --null 意味着评级未知或者尚未分配。

StudentID 属性是一个外键,Student 是与其且对应的导航属性。 Enrollment 实体与一个 Student 实体相关联,因此该属性只包含单个 Student 实体 (与前面所看到的 Student.Enrollments 导航属性不同后,`Student`中可以容纳多个 Enrollment 实体)。

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

如果一个属性名为 <navigation property name><primary key property name>,Entity Framework 就会将这个属性解析为外键属性(例如, Student 实体的主键是IDStudent 是`Enrollment`的导航属性所以`Enrollment`实体中 StudentID 会被解析为外键)。 此外还可以将需要解析为外键的属性命名为 <primary key property name> (例如,CourseID 由于 Course 实体的主键所以 CourseID 也被解析为外键)。

Course 实体

Course 实体关系图

Models 文件夹中,创建 Course.cs 并且用以下代码替换现有代码:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Course
    {
        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        public int CourseID { get; set; }
        public string Title { get; set; }
        public int Credits { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

Enrollments 属性是导航属性。 一个 Course 体可以与任意数量的 Enrollment 实体相关。

我们在本系列 后面的教程 中会有更多有关 DatabaseGenerated 特性的例子。 简单来说,此特性让你能自行指定主键,而不是让数据库自动指定主键。

创建数据库上下文

使得给定的数据模型与 Entity Framework 功能相协调的主类是数据库上下文类。 可以通过继承 Microsoft.EntityFrameworkCore.DbContext 类的方式创建此类。 在该类中你可以指定数据模型中包含哪些实体。 你还可以定义某些 Entity Framework 行为。 在此项目中将数据库上下文类命名为 SchoolContext

在项目文件夹中,创建名为的文件夹 Data

Data 文件夹创建名为 SchoolContext.cs 的类文件,并将模板代码替换为以下代码:

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
    }
}

此代码将为每个实体集创建 DbSet 属性。 在 Entity Framework 中,实体集通常与数据表相对应,具体实体与表中的行相对应。

在这里可以省略 DbSet<Enrollment>DbSet<Course> 语句,实现的功能没有任何改变。 Entity Framework 会隐式包含这两个实体因为 Student 实体引用了 Enrollment 实体、Enrollment 实体引用了 Course 实体。

当数据库创建完成后, EF 创建一系列数据表,表名默认和 DbSet 属性名相同。 集合属性的名称一般使用复数形式,但不同的开发人员的命名习惯可能不一样,开发人员根据自己的情况确定是否使用复数形式。 在定义 DbSet 属性的代码之后,添加下面高亮代码,对 DbContext 指定单数的表名来覆盖默认的表名。 此教程在最后一个 DbSet 属性之后,添加以下高亮显示的代码。

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
        }
    }
}

生成项目以检查编译器错误。

注册 SchoolContext

ASP.NET Core 默认实现 依赖注入。 在应用程序启动过程通过依赖注入注册相关服务 (例如 EF 数据库上下文)。 需要这些服务的组件 (如 MVC 控制器) 可以通过向构造函数添加相关参数来获得对应服务。 在本教程后面你将看到控制器构造函数的代码,就是通过上述方式获得上下文实例。

若要将 SchoolContext 注册为一种服务,打开 Startup.cs,并将突出显示的代码行添加到 ConfigureServices 方法中。

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

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

    services.AddMvc();
}

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

添加 using 语句引用 ContosoUniversity.DataMicrosoft.EntityFrameworkCore 命名空间,然后生成项目。

using ContosoUniversity.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.AspNetCore.Http;

打开 appsettings.json 文件,并按以下所示添加连接字符串。

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity1;Trusted_Connection=True;MultipleActiveResultSets=true"
  },
  "Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Warning"
    }
  }
}

SQL Server Express LocalDB

数据库连接字符串指定使用 SQL Server LocalDB 数据库。 LocalDB 是 SQL Server Express 数据库引擎的轻量级版本,用于应用程序开发,不在生产环境中使用。 LocalDB 作为按需启动并在用户模式下运行的轻量级数据库没有复杂的配置。 默认情况下, LocalDB 在 C:/Users/<user> 目录下创建 .mdf 数据库文件。

使用测试数据初始化数据库

Entity Framework 已经为你创建了一个空数据库。 在本部分中,你将编写一个方法用于向数据库填充测试数据,该方法会在数据库创建完成之后执行。

此处将使用 EnsureCreated 方法来自动创建数据库。 在 后面的教程 你将了解如何通过使用 Code First Migration 来更改而不是删除并重新创建数据库来处理模型更改。

Data 文件夹中,创建名为的新类文件 DbInitializer.cs 并且将模板代码替换为以下代码,使得在需要时能创建数据库并向其填充测试数据。

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

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            context.Database.EnsureCreated();

            // Look for any students.
            if (context.Students.Any())
            {
                return;   // DB has been seeded
            }

            var students = new Student[]
            {
            new Student{FirstMidName="Carson",LastName="Alexander",EnrollmentDate=DateTime.Parse("2005-09-01")},
            new Student{FirstMidName="Meredith",LastName="Alonso",EnrollmentDate=DateTime.Parse("2002-09-01")},
            new Student{FirstMidName="Arturo",LastName="Anand",EnrollmentDate=DateTime.Parse("2003-09-01")},
            new Student{FirstMidName="Gytis",LastName="Barzdukas",EnrollmentDate=DateTime.Parse("2002-09-01")},
            new Student{FirstMidName="Yan",LastName="Li",EnrollmentDate=DateTime.Parse("2002-09-01")},
            new Student{FirstMidName="Peggy",LastName="Justice",EnrollmentDate=DateTime.Parse("2001-09-01")},
            new Student{FirstMidName="Laura",LastName="Norman",EnrollmentDate=DateTime.Parse("2003-09-01")},
            new Student{FirstMidName="Nino",LastName="Olivetto",EnrollmentDate=DateTime.Parse("2005-09-01")}
            };
            foreach (Student s in students)
            {
                context.Students.Add(s);
            }
            context.SaveChanges();

            var courses = new Course[]
            {
            new Course{CourseID=1050,Title="Chemistry",Credits=3},
            new Course{CourseID=4022,Title="Microeconomics",Credits=3},
            new Course{CourseID=4041,Title="Macroeconomics",Credits=3},
            new Course{CourseID=1045,Title="Calculus",Credits=4},
            new Course{CourseID=3141,Title="Trigonometry",Credits=4},
            new Course{CourseID=2021,Title="Composition",Credits=3},
            new Course{CourseID=2042,Title="Literature",Credits=4}
            };
            foreach (Course c in courses)
            {
                context.Courses.Add(c);
            }
            context.SaveChanges();

            var enrollments = new Enrollment[]
            {
            new Enrollment{StudentID=1,CourseID=1050,Grade=Grade.A},
            new Enrollment{StudentID=1,CourseID=4022,Grade=Grade.C},
            new Enrollment{StudentID=1,CourseID=4041,Grade=Grade.B},
            new Enrollment{StudentID=2,CourseID=1045,Grade=Grade.B},
            new Enrollment{StudentID=2,CourseID=3141,Grade=Grade.F},
            new Enrollment{StudentID=2,CourseID=2021,Grade=Grade.F},
            new Enrollment{StudentID=3,CourseID=1050},
            new Enrollment{StudentID=4,CourseID=1050},
            new Enrollment{StudentID=4,CourseID=4022,Grade=Grade.F},
            new Enrollment{StudentID=5,CourseID=4041,Grade=Grade.C},
            new Enrollment{StudentID=6,CourseID=1045},
            new Enrollment{StudentID=7,CourseID=3141,Grade=Grade.A},
            };
            foreach (Enrollment e in enrollments)
            {
                context.Enrollments.Add(e);
            }
            context.SaveChanges();
        }
    }
}

这段代码首先检查是否有学生数据在数据库中,如果没有的话,就可以假定数据库是新建的,然后使用测试数据进行填充。 代码中使用数组存放测试数据而不是使用 List<T> 集合是为了优化性能。

Program.cs 中修改 Main 方法,使得在应用程序启动时能执行以下操作:

  • 从依赖注入容器中获取数据库上下文实例。
  • 调用 seed 方法,将上下文传递给它。
  • Seed 方法完成此操作时释放上下文。
using ContosoUniversity.Data;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;

namespace ContosoUniversity
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var host = CreateHostBuilder(args).Build();

            CreateDbIfNotExists(host);

            host.Run();
        }

        private static void CreateDbIfNotExists(IHost host)
        {
            using (var scope = host.Services.CreateScope())
            {
                var services = scope.ServiceProvider;
                try
                {
                    var context = services.GetRequiredService<SchoolContext>();
                    DbInitializer.Initialize(context);
                }
                catch (Exception ex)
                {
                    var logger = services.GetRequiredService<ILogger<Program>>();
                    logger.LogError(ex, "An error occurred creating the DB.");
                }
            }
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(webBuilder =>
                {
                    webBuilder.UseStartup<Startup>();
                });
    }
}

首次运行该应用程序时,将创建数据库并使用测试数据填充该数据库。 每次更改数据模型时:

  • 删除数据库。
  • 更新 Seed 方法,并以相同的方式用新数据库重新开始。

在之后的教程中,你将了解如何在数据模型更改时,只需修改数据库而无需删除重建数据库。

创建控制器和视图

在本节,将使用 Visual Studio 中的基架引擎添加一个 MVC 控制器,以及使用 EF 来查询和保存数据的视图。

CRUD 操作方法和视图的自动创建被称为基架。 基架与代码生成不同,基架的代码是一个起点,可以修改基架以满足自己需求,而你通常无需修改生成的代码。 当你需要自定义生成代码时,可使用一部分类或需求发生变化时重新生成代码。

  • 右键单击“解决方案资源管理器”中的“控制器”文件夹,然后选择“添加”>“新搭建基架的项目”。
  • 在“添加基架”对话框中
    • 选择 视图使用 Entity Framework 的 MVC 控制器
    • 单击 添加。 随即将显示“使用实体框架添加包含视图的 MVC 控制器”对话框:搭建“学生”基架
    • 模型类 选择 Student
    • 在“数据上下文类”中选择 SchoolContext 。
    • 使用 StudentsController 作为默认名称。
    • 单击 添加

Visual Studio 基架引擎创建 StudentsController.cs 文件和一组对应于控制器的视图(.cshtml 文件)。

注意控制器将 SchoolContext 作为构造函数参数。

namespace ContosoUniversity.Controllers
{
    public class StudentsController : Controller
    {
        private readonly SchoolContext _context;

        public StudentsController(SchoolContext context)
        {
            _context = context;
        }

ASP.NET Core 依赖关系注入负责将 SchoolContext 实例传递到控制器。 这是在 Startup.cs 文件中配置的。

控制器包含 Index 操作方法,用于显示数据库中的所有学生。 该方法从学生实体集中获取学生列表,学生实体集则是通过读取数据库上下文实例中的 Students 属性获得:

public async Task<IActionResult> Index()
{
    return View(await _context.Students.ToListAsync());
}

本教程后面部分将介绍此代码中的异步编程元素。

Views/Students/Index.cshtml 视图使用table标签显示此列表:

@model IEnumerable<ContosoUniversity.Models.Student>

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                <th>
                    @Html.DisplayNameFor(model => model.LastName)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.FirstMidName)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.EnrollmentDate)
                </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.LastName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.FirstMidName)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.EnrollmentDate)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

按 CTRL+F5 来运行该项目,或从菜单选择“调试”>“开始执行(不调试)”。

单击学生选项卡以查看 DbInitializer.Initialize 插入的测试的数据。 你将看到 Students 选项卡链接在页的顶部或在单击右上角后的导航图标中,具体显示在哪里取决于浏览器窗口宽度。

Contoso University home 页窄

“学生索引”页

查看数据库

当你启动了应用程序,DbInitializer.Initialize 方法调用 EnsureCreated。 EF 没有检测到相关数据库,因此自己创建了一个,接着 Initialize 方法的其余代码向数据库中填充数据。 你可以使用 Visual Studio 中的 SQL Server 对象资源管理器 (SSOX) 查看数据库。

关闭浏览器。

如果 SSOX 窗口尚未打开,请从Visual Studio 中的视图 菜单中选择。

在 SSOX 中,单击“(localdb)\MSSQLLocalDB”>“数据库”,然后单击 appsettings.json 文件中的连接字符串中的数据库名称的实体。

展开“表”节点,查看数据库中的表。

SSOX 中的表

右键单击 Student 表,然后单击 查看数据,即可查看已创建的列和已插入到表的行。

SSOX 中的 Student 表

.mdf 和 .ldf 数据库文件位于“C:\Users\<username>”文件夹中。

因为调用 EnsureCreated 的初始化方法在启动应用程序时才运行,所以在这之前你可以更改 Student 类、 删除数据库、 再运行一次应用程序,这时候数据库将自动重新创建,以匹配所做的更改。 例如,如果向 Student 类添加 EmailAddress 属性,重新的创建表中会有 EmailAddress 列。

约定

由于 Entity Framework 有一定的约束条件,你只需要按规则编写很少的代码就能够创建一个完整的数据库。

  • DbSet 类型的属性用作表名。 如果实体未被 DbSet 属性引用,实体类名称用作表名称。
  • 使用实体属性名作为列名。
  • 以 ID 或 classnameID 命名的实体属性被视为主键属性。
  • 如果属性名为 <导航属性名><主键属性名>(例如,StudentID 对应 Student 导航属性,因为 Student 实体的主键是 ID),则其将被解释为外键属性。 此外还可以将外键属性仅命名为 <主键属性名>(例如 EnrollmentID,因为 Enrollment 实体的主键为 EnrollmentID)。

约定行为可以重写。 例如,本教程前半部分显式指定表名称。 本系列 后面教程 则设置列名称并将任何属性设置为主键或外键。

异步代码

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

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

异步代码在运行时,会引入的少量开销,在低流量时对性能的影响可以忽略不计,但在针对高流量情况下潜在的性能提升是可观的。

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

public async Task<IActionResult> Index()
{
    return View(await _context.Students.ToListAsync());
}
  • async 关键字用于告知编译器该方法主体将生成回调并自动创建 Task<IActionResult> 返回对象。
  • 返回类型 Task<IActionResult> 表示正在进行的工作返回的结果为 IActionResult 类型。
  • await 关键字会使得编译器将方法拆分为两个部分。 第一部分是以异步方式结束已启动的操作。 第二部分是当操作完成时注入调用回调方法的地方。
  • ToListAsyncToList 方法的的异步扩展版本。

使用 Entity Framework 编写异步代码时的一些注意事项:

  • 只有导致查询或发送数据库命令的语句才能以异步方式执行。 包括 ToListAsyncSingleOrDefaultAsync,和 SaveChangesAsync。 不包括只需更改 IQueryable 的语句,如 var students = context.Students.Where(s => s.LastName == "Davolio")
  • EF 上下文是线程不安全的: 请勿尝试并行执行多个操作。 当调用异步 EF 方法时,始终使用 await 关键字。
  • 如果你想要利用异步代码的性能优势,请确保你所使用的任何库和包在它们调用导致 Entity Framework 数据库查询方法时也使用异步。

有关在 .NET 异步编程的详细信息,请参阅 异步概述

后续步骤

请继续阅读下一篇教程,了解如何执行基本的 CRUD(创建、读取、更新、删除)操作。