教程:学习高级场景 - 使用 EF Core 和 ASP.NET MVC

之前的教程中,已经以每个类一张表的方式实现了继承。 本教程介绍了在超越开发使用 Entity Framework Core ASP.NET Core Web 应用程序的基础知识时,需要注意的几个主题。

在本教程中,你将:

  • 执行原始 SQL 查询
  • 调用查询以返回实体
  • 调用查询以获取其他类型的返回值
  • 调用更新查询
  • 检查 SQL 查询
  • 创建抽象层
  • 了解自动更改检测
  • 了解 EF Core 源代码和开发计划
  • 了解如何使用动态 LINQ 简化代码

先决条件

执行原始 SQL 查询

使用 Entity Framework 的优点之一是,它避免将代码与存储数据的特定方法太接近。 它通过为你生成 SQL 查询和命令来执行此操作,这也使你无需自行编写它们。 但是,需要运行手动创建的特定 SQL 查询时,会出现异常情况。 对于这些方案,Entity Framework Code First API 包括使你能够将 SQL 命令直接传递到数据库的方法。 EF Core 1.0 中有以下选项:

  • 对返回实体类型的查询使用 DbSet.FromSql 方法。 返回的对象必须是 DbSet 对象预期的类型,除非 关闭跟踪,否则数据库上下文会自动跟踪这些对象。

  • 对非查询命令使用 Database.ExecuteSqlCommand

如果您需要运行返回非实体类型的查询,可以使用 ADO.NET 结合 EF 提供的数据库连接。 即使使用此方法检索实体类型,数据库上下文也不会跟踪返回的数据。

在 Web 应用程序中执行 SQL 命令时,必须采取预防措施来保护站点免受 SQL 注入攻击。 这样做的一种方法是使用参数化查询来确保网页提交的字符串不能解释为 SQL 命令。 在本教程中,你将在将用户输入集成到查询时使用参数化查询。

调用查询以返回实体

DbSet<TEntity> 类提供了一种方法,可用于执行返回 TEntity类型的实体的查询。 若要查看此工作原理,你将在部门控制器的 Details 方法中更改代码。

DepartmentsController.csDetails 方法中,使用 FromSql 方法调用替换检索院系的代码,如以下突出显示的代码所示:

public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    string query = "SELECT * FROM Department WHERE DepartmentID = {0}";
    var department = await _context.Departments
        .FromSql(query, id)
        .Include(d => d.Administrator)
        .AsNoTracking()
        .FirstOrDefaultAsync();

    if (department == null)
    {
        return NotFound();
    }

    return View(department);
}

若要验证新代码是否正常工作,请选择 部门 选项卡,然后选择其中一个部门的 详情

部门详细信息

调用查询以返回其他类型

之前,你为“关于”页创建了一个学生统计信息网格,其中显示了每个注册日期的学生数。 你从 Students 实体集(_context.Students)获取了数据,并使用 LINQ 将结果投影到 EnrollmentDateGroup 视图模型对象列表中。 假设你想要编写 SQL 本身,而不是使用 LINQ。 为此,需要运行返回实体对象以外的其他项的 SQL 查询。 在 EF Core 1.0 中,一种方法是编写 ADO.NET 代码并从 EF 获取数据库连接。

HomeController.cs中,将 About 方法替换为以下代码:

public async Task<ActionResult> About()
{
    List<EnrollmentDateGroup> groups = new List<EnrollmentDateGroup>();
    var conn = _context.Database.GetDbConnection();
    try
    {
        await conn.OpenAsync();
        using (var command = conn.CreateCommand())
        {
            string query = "SELECT EnrollmentDate, COUNT(*) AS StudentCount "
                + "FROM Person "
                + "WHERE Discriminator = 'Student' "
                + "GROUP BY EnrollmentDate";
            command.CommandText = query;
            DbDataReader reader = await command.ExecuteReaderAsync();

            if (reader.HasRows)
            {
                while (await reader.ReadAsync())
                {
                    var row = new EnrollmentDateGroup { EnrollmentDate = reader.GetDateTime(0), StudentCount = reader.GetInt32(1) };
                    groups.Add(row);
                }
            }
            reader.Dispose();
        }
    }
    finally
    {
        conn.Close();
    }
    return View(groups);
}

添加 using 语句:

using System.Data.Common;

运行应用并转到“关于”页面。 它显示与之前相同的数据。

关于页面

调用更新查询

假设 Contoso 大学管理员希望在数据库中执行全局更改,例如更改每个课程的学分数。 如果大学有大量的课程,检索它们作为实体并单独更改它们会效率低下。 在本部分中,你将实现一个网页,使用户能够指定一个因素来更改所有课程的学分数,并通过执行 SQL UPDATE 语句进行更改。 网页将如下图所示:

更新课程学分页面

CoursesController.cs中,为 HttpGet 和 HttpPost 添加 UpdateCourseCredits 方法:

public IActionResult UpdateCourseCredits()
{
    return View();
}
[HttpPost]
public async Task<IActionResult> UpdateCourseCredits(int? multiplier)
{
    if (multiplier != null)
    {
        ViewData["RowsAffected"] = 
            await _context.Database.ExecuteSqlCommandAsync(
                "UPDATE Course SET Credits = Credits * {0}",
                parameters: multiplier);
    }
    return View();
}

当控制器处理 HttpGet 请求时,ViewData["RowsAffected"]中不返回任何内容,视图显示空文本框和提交按钮,如上图所示。

当单击Update按钮时,将调用 HttpPost 方法,且从文本框中输入的值获取乘数。 然后,该代码执行更新课程的 SQL,并将受影响的行数返回到 ViewData中的视图。 当视图获取 RowsAffected 值时,它将显示更新的行数。

在“解决方案资源管理器”中,右键单击“Views/Courses”文件夹,然后依次单击“添加”>“新建项”

添加新项对话框中,在左侧窗格的已安装下单击 ASP.NET Core,单击Razor 视图,并将新视图命名为 UpdateCourseCredits.cshtml

Views/Courses/UpdateCourseCredits.cshtml中,将模板代码替换为以下代码:

@{
    ViewBag.Title = "UpdateCourseCredits";
}

<h2>Update Course Credits</h2>

@if (ViewData["RowsAffected"] == null)
{
    <form asp-action="UpdateCourseCredits">
        <div class="form-actions no-color">
            <p>
                Enter a number to multiply every course's credits by: @Html.TextBox("multiplier")
            </p>
            <p>
                <input type="submit" value="Update" class="btn btn-default" />
            </p>
        </div>
    </form>
}
@if (ViewData["RowsAffected"] != null)
{
    <p>
        Number of rows updated: @ViewData["RowsAffected"]
    </p>
}
<div>
    @Html.ActionLink("Back to List", "Index")
</div>

通过选择 课程 选项卡,然后将“/UpdateCourseCredits”添加到浏览器地址栏中 URL 末尾(例如:http://localhost:5813/Courses/UpdateCourseCredits),运行 UpdateCourseCredits 方法。 在文本框中输入数字:

更新课程学分页面

单击 更新。 可以看到受影响的行数:

“更新课程学分”页面中受影响的行

单击“返回列表”可以查看课程列表,其中学分已替换为修改后的数字。

请注意,生产代码将确保更新始终导致有效的数据。 此处所示的简化代码可以将学分数量提高到大于 5 的数值。 (Credits 属性具有 [Range(0, 5)] 属性。更新查询将有效,但无效数据可能会导致系统其他部分出现意外结果,这些结果假定信用额度数为 5 或更少。

有关原始 SQL 查询的详细信息,请参阅 原始 SQL 查询

检查 SQL 查询

有时,能够查看发送到数据库的实际 SQL 查询会很有帮助。 EF Core 自动使用 ASP.NET Core 的内置日志记录功能来编写包含 SQL 的查询和更新的日志。 在本部分中,你将看到 SQL 日志记录的一些示例。

打开 StudentsController.cs 并在 Details 方法中设置 if (student == null) 语句上的断点。

在调试模式下运行应用,并转到学生的“详细信息”页。

转到输出窗口显示调试输出,就可以看到查询语句:

Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (56ms) [Parameters=[@__id_0='?'], CommandType='Text', CommandTimeout='30']
SELECT TOP(2) [s].[ID], [s].[Discriminator], [s].[FirstName], [s].[LastName], [s].[EnrollmentDate]
FROM [Person] AS [s]
WHERE ([s].[Discriminator] = N'Student') AND ([s].[ID] = @__id_0)
ORDER BY [s].[ID]
Microsoft.EntityFrameworkCore.Database.Command:Information: Executed DbCommand (122ms) [Parameters=[@__id_0='?'], CommandType='Text', CommandTimeout='30']
SELECT [s.Enrollments].[EnrollmentID], [s.Enrollments].[CourseID], [s.Enrollments].[Grade], [s.Enrollments].[StudentID], [e.Course].[CourseID], [e.Course].[Credits], [e.Course].[DepartmentID], [e.Course].[Title]
FROM [Enrollment] AS [s.Enrollments]
INNER JOIN [Course] AS [e.Course] ON [s.Enrollments].[CourseID] = [e.Course].[CourseID]
INNER JOIN (
    SELECT TOP(1) [s0].[ID]
    FROM [Person] AS [s0]
    WHERE ([s0].[Discriminator] = N'Student') AND ([s0].[ID] = @__id_0)
    ORDER BY [s0].[ID]
) AS [t] ON [s.Enrollments].[StudentID] = [t].[ID]
ORDER BY [t].[ID]

你会注意到一些可能令你吃惊的事情:SQL 从 Person 表中选择最多 2 行(TOP(2))。 SingleOrDefaultAsync方法服务器上不会解析为 1 行。 原因如下:

  • 如果查询将返回多行,该方法将返回 null。
  • 若要确定查询是否返回多行,EF 必须检查它是否至少返回 2。

请注意,你不必使用调试模式,并在断点处停止,然后在输出窗口获取日志记录。 这只是在想要查看输出时停止日志记录的一种便捷方法。 如果不这样做,日志记录会继续,你必须向上滚动才能找到你感兴趣的部分。

创建抽象层

许多开发人员编写代码以将存储库和工作单元模式实现为与 Entity Framework 配合使用的代码包装器。 这些模式旨在创建数据访问层与应用程序的业务逻辑层之间的抽象层。 实现这些模式有助于使应用程序免受数据存储中的更改的隔离,并有助于自动化单元测试或测试驱动开发(TDD)。 但是,编写其他代码来实现这些模式并不总是使用 EF 的应用程序的最佳选择,原因有多种:

  • EF 上下文类本身将代码与特定于数据存储的代码隔离开来。

  • EF 上下文类可以充当使用 EF 进行的数据库更新的一个工作单元类。

  • EF 包括用于在不编写存储库代码的情况下实现 TDD 的功能。

有关如何实现存储库和工作单元模式的信息,请参阅本教程系列 Entity Framework 5 版本。

Entity Framework Core 实现可用于测试的内存中数据库提供程序。 有关详细信息,请参阅测试以及 InMemory

自动脏值检测

实体框架通过将实体的当前值与原始值进行比较来确定实体的更改方式(因此需要将哪些更新发送到数据库)。 查询或附加实体时,将存储原始值。 导致自动更改检测的一些方法如下:

  • DbContext.SaveChanges

  • DbContext.Entry

  • ChangeTracker.Entries

如果要跟踪大量实体,并在循环中多次调用其中一种方法,则可以通过使用 ChangeTracker.AutoDetectChangesEnabled 属性暂时关闭自动更改检测来获得显著的性能改进。 例如:

_context.ChangeTracker.AutoDetectChangesEnabled = false;

EF Core 源代码和开发计划

Entity Framework Core 源代码 位于 https://github.com/dotnet/efcore。 EF Core 存储库包含夜间生成、问题跟踪、功能规格、设计会议说明,以及 未来开发路线图。 您可以提交或查找错误,还可以参与贡献。

虽然源代码已打开,但 Entity Framework Core 作为Microsoft产品完全受支持。 Microsoft Entity Framework 团队可控制接受哪些贡献,并测试所有代码更改,以确保每个版本的质量。

现有数据库逆向工程

若要反向工程数据模型(包括现有数据库中的实体类),请使用 基架-dbcontext 命令。 请参阅 入门教程

使用动态 LINQ 简化代码

本系列 第三个教程演示如何通过在 switch 语句中硬编码列名来编写 LINQ 代码。 有两列可供选择,这样做没问题,但如果有很多列,代码可能会变得冗长。 若要解决此问题,可以使用 EF.Property 方法将属性的名称指定为字符串。 若要试用此方法,请将 StudentsController 中的 Index 方法替换为以下代码。

 public async Task<IActionResult> Index(
     string sortOrder,
     string currentFilter,
     string searchString,
     int? pageNumber)
 {
     ViewData["CurrentSort"] = sortOrder;
     ViewData["NameSortParm"] = 
         String.IsNullOrEmpty(sortOrder) ? "LastName_desc" : "";
     ViewData["DateSortParm"] = 
         sortOrder == "EnrollmentDate" ? "EnrollmentDate_desc" : "EnrollmentDate";

     if (searchString != null)
     {
         pageNumber = 1;
     }
     else
     {
         searchString = currentFilter;
     }

     ViewData["CurrentFilter"] = searchString;

     var students = from s in _context.Students
                    select s;
     
     if (!String.IsNullOrEmpty(searchString))
     {
         students = students.Where(s => s.LastName.Contains(searchString)
                                || s.FirstMidName.Contains(searchString));
     }

     if (string.IsNullOrEmpty(sortOrder))
     {
         sortOrder = "LastName";
     }

     bool descending = false;
     if (sortOrder.EndsWith("_desc"))
     {
         sortOrder = sortOrder.Substring(0, sortOrder.Length - 5);
         descending = true;
     }

     if (descending)
     {
         students = students.OrderByDescending(e => EF.Property<object>(e, sortOrder));
     }
     else
     {
         students = students.OrderBy(e => EF.Property<object>(e, sortOrder));
     }

     int pageSize = 3;
     return View(await PaginatedList<Student>.CreateAsync(students.AsNoTracking(), 
         pageNumber ?? 1, pageSize));
 }

致谢

汤姆·迪克斯特拉和里克·安德森(推特 @RickAndMSFT) 写了本教程。 Rowan Miller、Diego Vega 和 Entity Framework 团队的其他成员协助进行代码评审,并帮助调试在为教程编写代码时出现的问题。 John Parente 和 Paul Goldman 致力于更新 ASP.NET Core 2.2 的教程。

排查常见错误

ContosoUniversity.dll 被另一个进程使用

错误信息:

无法打开“...bin\Debug\netcoreapp1.0\ContosoUniversity.dll' for writing -- 'The process cannot access the file '...\bin\Debug\netcoreapp1.0\ContosoUniversity.dll',因为它正由另一个进程使用。

解决方案:

停止 IIS Express 中的站点。 请转到 Windows 系统任务栏中,找到 IIS Express 并右键单击其图标、 选择 Contoso 大学站点,然后单击停止站点

迁移基架的 Up 和 Down 方法中没有代码

可能的原因:

EF CLI 命令不会自动关闭并保存代码文件。 如果在运行 migrations add 命令时未保存更改,EF 将找不到更改。

解决方案:

运行 migrations remove 命令,保存代码更改并重新运行 migrations add 命令。

运行数据库更新时出错

在具有现有数据的数据库中进行架构更改时,可能会收到其他错误。 如果遇到无法解决的迁移错误,可以更改连接字符串中的数据库名称或删除数据库。 使用新数据库时,没有要迁移的数据,并且 update-database 命令更有可能在没有错误的情况下完成。

最简单的方法是重命名 appsettings.json中的数据库。 下次运行 database update时,将创建一个新数据库。

若要删除 SSOX 中的数据库,请右键单击该数据库,单击 “删除”,然后在“删除数据库”对话框中,选择“关闭现有连接”,然后单击 “确定”

若要使用 CLI 删除数据库,请运行 database drop CLI 命令:

dotnet ef database drop

查找 SQL Server 实例时出错

错误信息:

与 SQL Server 建立连接时发生与网络相关的或特定于实例的错误。 找不到服务器或无法访问服务器。 验证实例名称是否正确,并且是否已将 SQL Server 配置为允许远程连接。 (提供程序:SQL 网络接口,错误:26 - 查找指定的服务器/实例时出错)

解决方案:

检查连接字符串。 如果已手动删除数据库文件,请更改构造字符串中的数据库名称以以以新数据库开头。

获取代码

下载或查看已完成的应用程序。

其他资源

有关 EF Core的详细信息,请参阅 Entity Framework Core 文档。 还提供:Entity Framework Core 实战一书。

有关如何部署 Web 应用的信息,请参阅 主机和部署 ASP.NET Core

有关与 ASP.NET Core MVC(如身份验证和授权)相关的其他主题的信息,请参阅 ASP.NET Core 概述。

有关创建可靠、安全、高性能、可测试且可缩放的 ASP.NET Core 应用的指导,请参阅 企业 Web 应用模式。 可以使用完整生产质量的样例网络应用程序来实现这些模式。

后续步骤

在本教程中,你将:

  • 执行原始 SQL 查询
  • 已调用查询以返回实体
  • 调用查询以返回其他类型
  • 已调用更新查询
  • 检查的 SQL 查询
  • 创建了抽象层
  • 已了解自动更改检测
  • 了解了 EF Core 源代码和开发计划
  • 了解如何使用动态 LINQ 简化代码

完成本系列教程,介绍如何在 ASP.NET Core MVC 应用程序中使用 Entity Framework Core。 本系列使用的是新建数据库;另一种方式是从现有数据库进行模型的反向工程