四、实体框架核心的数据模型

我们从第 1 章*准备开始使用的HealthCheck示例应用运行良好,但缺少一些我们可能在典型 web 应用中使用的重要功能;其中最重要的是能够从数据库管理系统DBMS中读取和写入数据,因为这是几乎所有与 web 相关的任务的基本要求:内容管理、知识共享、即时通信、数据存储和/或挖掘、跟踪和统计,用户身份验证、系统日志记录等。*

**说实话,即使是我们的HealthCheck应用也肯定可以使用其中的一些任务:随着时间的推移跟踪主机状态可能是一个不错的功能;用户认证应该是必须具备的,特别是如果我们计划将其公开发布到 web 上;系统日志记录总是很棒的;等等然而,由于我们更喜欢保持项目尽可能简单,我们将创建一个新项目,并向其授予一些 DBMS 功能。

这是我们在本章要做的:

  • 创建一个全新的.NET Core 3 和 Angular web 应用项目,名为WorldCities:世界各地城市的数据库
  • 选择合适的数据源获取合理数量的真实数据进行播放
  • 使用实体框架核心定义并实现数据模型
  • 配置并部署我们项目将使用的 DBMS 引擎
  • 使用实体框架核心的数据迁移功能创建数据库
  • 执行数据播种策略将数据源加载到数据库
  • 使用实体框架核心提供的对象关系映射ORM技术,通过与.NET Core 进行数据读写

你准备好开始了吗?

技术要求

在本章中,我们需要前面章节中列出的所有先前的技术要求,以及以下外部库:

  • Microsoft.EntityFrameworkCoreNuGet 套餐
  • Microsoft.EntityFrameworkCore.ToolsNuGet 套餐
  • Microsoft.EntityFrameworkCore.SqlServerNuGet 套餐
  • SQL Server 2019(如果我们选择本地 SQL 实例路由)
  • MS Azure 订阅(如果我们选择云数据库托管路由)

和往常一样,建议不要直接安装它们。我们将在本章中介绍它们,以便我们可以在项目中对它们的用途进行上下文分析。

本章的代码文件可在中找到 https://github.com/PacktPublishing/ASP.NET-Core-3-and-Angular-9-Third-Edition/tree/master/Chapter_04/

世界城市网络应用

我们要做的第一件事是创建一个新的.NET Core 和 Angular web 应用项目。还记得我们在第 1 章准备的第二部分所做的事情吗?我们可以做同样的事情(并对我们在第 2 章环视中所做的示例项目进行所有相关更改),或者将我们现有的HealthCheck项目复制到另一个文件夹,将所有引用重命名为HealthCheck(源代码文件系统),并撤销我们在第 2 章环视第 3 章前端和后端交互中所做的一切。****

**尽管两种方法都很好,但前一种方法肯定更实用,更不用说这是一个很好的机会,可以将我们迄今为止学到的知识付诸实践,并确保我们理解了每个相关步骤。

让我们简要回顾一下我们需要做什么:

  1. 使用dotnet new angular -o WorldCities命令创建一个新项目。
  2. 编辑或删除以下.NET Core后端文件:
    • Startup.cs(编辑)
    • WeatherForecast.cs(删除)
    • /Controllers/WeatherForecastController.cs(删除)
  3. 编辑或删除以下 Angular前端文件:
    • /ClientApp/package.json(编辑)
    • /ClientApp/src/app/app.module.ts(编辑)
    • /ClientApp/src/app/nav-menu/nav-menu.component.html(编辑)
    • /ClientApp/src/app/counter/(删除整个文件夹)
    • /ClientApp/src/app/fetch-data/(删除整个文件夹)

In the unlikely case that we choose to copy and paste the HealthCheck project—which we don't recommend, we would need to remove the HealthChecks middleware references from the Startup.cs file and the Angular Components references within the various Angular configuration files. We would also have to delete the related .NET and Angular class files (ICMPHealthCheckCustomHealthCheckOptions, the /ClientApp/src/app/health-check/ folder, and so on).

As we can see, cloning a project would mean that we would have to perform a lot of undo and/or rename activities: this is precisely why starting from scratch is generally a better approach.

在完成所有这些更改后,我们可以通过按F5并检查结果来检查一切是否正常。如果一切正常,我们应该能够看到以下屏幕:

就是这样:现在,我们有一个全新的.NET Core+Angular web 应用可供使用。我们只需要一个数据源和一个数据模型,可以通过后端 web API访问,从以下位置检索一些数据:换句话说,数据服务器。

使用数据服务器的原因

在我们继续之前,花几分钟回答以下问题是明智的:我们真的需要真正的数据服务器吗?我们就不能模仿一个吗?毕竟,我们只运行代码示例。

事实上,我们完全可以避免这样做,跳过整个章节:Angular 提供了一个内存 Web API包,它取代了HttpClient模块的HttpBackend并在 RESTful API 上模拟CRUD操作;仿真是通过截获 Angular HTTP 请求并将它们重定向到我们控制下的内存中数据存储来执行的。

此包非常好,适用于大多数测试用例场景,例如:

  • 模拟针对尚未在开发/测试服务器上实现的数据收集的操作
  • 编写读写数据的单元测试应用,而无需拦截多个 HTTP 调用和生成响应序列
  • 在不干扰真实数据库的情况下执行端到端测试,这对于C持续集成CI构建非常有用

内存中的 Web API 服务工作得非常好,整个 Angular 文档都位于https://angular.io/ 依赖它。然而,我们现在不打算使用它,原因很简单(也很明显):本书的重点不是 Angular,而是 Angular 和.NET Core 之间的客户端/服务器互操作性;正是出于这个原因,开发一个真实的 Web API 并通过真实的数据模型将其连接到真实的数据源是游戏的一部分。

我们不想模拟 RESTful后端的行为,因为我们需要了解那里发生了什么以及如何正确地实现它:我们想要实现它,以及承载和提供数据的 DBMS。

这正是我们要做的,从下一节开始。

Those who want to get additional information about the Angular In-memory Web API service can visit the in-memory-web-api GitHub project page at https://github.com/angular/in-memory-web-api/.

数据源

我们的WorldCitiesweb 应用将提供什么样的数据?我们已经知道答案:一个来自世界各地的城市数据库。这样的存储库还存在吗?

事实上,我们可以使用几种替代方法来填充数据库,然后将其提供给最终用户。

以下是 DSpace CRIS 的自由世界城市数据库:

以下是 GeoDataSource 的世界城市数据库(免费版):

以下是 simplemaps.com 提供的世界城市数据库:

所有这些替代方案都足以满足我们的需要:我们将使用后者,因为它不需要注册,并且提供了一种可读的电子表格格式。

打开您喜爱的浏览器,键入或复制上述 URL,然后查找“世界城市数据库”部分的“基本”列:

单击下载按钮检索包含.csv.xlsx文件的(巨大)ZIP 文件,并将其保存在某处。现在就这样;我们稍后会处理这些问题。

从下一节开始,我们将开始数据模型的构建过程:这将是一个漫长但非常有益的旅程。

数据模型

现在我们有了原始数据源,我们需要找到一种方法使其可用于我们的 web 应用,以便我们的用户能够检索(并可能更改)实际数据。

为了简单起见,我们不会浪费宝贵的时间来介绍整个数据模型的概念,以及这两个词的各种含义。你们这些有经验的人,以及经验丰富的开发人员,可能会知道所有相关的东西。我们只想说,当我们谈论数据模型时,我们所指的并不是更多或更少的一组轻量级、明确类型化的实体类,这些实体类表示持久的、代码驱动的数据结构,我们可以将其用作 Web API 代码中的资源。

使用 persistent 这个词是有原因的;我们希望数据结构存储在数据库中。这对于任何基于数据的应用来说都是显而易见的。我们将要创建的全新 web 应用不会例外,因为我们希望它充当记录的集合或存储库,以便我们可以根据需要读取、创建、删除和/或修改。

我们很容易猜到,所有这些任务都将由一些由前端UI(Angular 组件)触发的后端业务逻辑(.NET 控制器)执行。

引入实体框架核心

我们将借助微软为ADO.NET开发的实体框架核心(也称EF 核心)和著名的开源对象关系映射器ORM)来创建我们的数据库。作出这种选择的原因如下:

  • 与 VisualStudioIDE 的无缝集成
  • 基于实体类(实体数据模型EDM)的概念模型,允许我们使用特定于域的对象处理数据,而无需编写数据访问代码,这正是我们所寻找的
  • 易于在开发和生产阶段部署、使用和维护
  • 兼容所有主流开源商业 SQL 和 NoSQL 引擎,包括MSSQLSQLiteAzure Cosmos DBPostgreSQLMySQL/MariaDBMyCATFirebirdDb2/InformixOracle DBMongoDB等,感谢通过 NuGet 提供的官方和/或第三方提供商和/或连接器

值得一提的是,实体框架核心之前被称为实体框架 7,直到其最新的 RC 版本。名称更改遵循了我们已经讨论过的 ASP.NET 5/ASP.NET Core 透视图切换,因为如果我们将其与前几期进行比较,它还强调了实体框架核心的主要重写/重新设计。

您可能想知道为什么我们选择采用基于 SQL 的方法,而不是使用 NoSQL 替代方案;有很多好的 NoSQL 产品,比如 MongoDB、RavenDB 和 CouchDB,它们碰巧有一个 C#连接器库。用其中一个来代替怎么样?

答案很简单:尽管可以作为第三方提供商使用,但它们尚未被纳入官方的实体框架核心数据库提供商列表(请参见以下信息框中的链接)。正是出于这个原因,我们将坚持使用关系数据库,这对于我们将在本书中设计的简单数据库模式来说可能是一种更方便的方法。

对于那些希望更多了解即将发布的版本和/或大胆使用它的人,我们强烈建议您查看以下链接和文档:

项目路线图https://github.com/aspnet/EntityFramework/wiki/Roadmap GitHub 上的源代码https://github.com/aspnet/EntityFramework

*正式文件*:https://docs.efproject.net/en/latest/ 官方实体框架核心数据库提供商列表https://docs.microsoft.com/en-us/ef/core/providers/?tabs=dotnet-核心 cli**

****# 安装实体框架核心

要安装 EntityFrameworkCore,我们需要将相关包添加到项目文件的 dependencies 部分。我们可以通过以下方式使用可视化 GUI 轻松完成此操作:

  1. 右键点击WorldCities项目。
  2. 选择 Manage NuGet Packages。
  3. 确保“包源”下拉列表设置为“全部”。
  4. 转到“浏览”选项卡,搜索包含Microsoft.EntityFrameworkCore关键字的软件包:

到达后,选择并安装以下软件包(最新版本,在撰写本文时):

  • Microsoft.EntityFrameworkCore版本 3.1.1
  • Microsoft.EntityFrameworkCore.Tools版本 3.1.1
  • Microsoft.EntityFrameworkCore.SqlServer版本 3.1.1

所有这些软件包还将带来一些必需的依赖项,我们还需要安装这些依赖项:

如果我们希望使用 NuGet package manager 命令行执行此操作,则可以输入以下内容:

PM> Install-Package Microsoft.EntityFrameworkCore -Version 3.1.1
PM> Install-Package Microsoft.EntityFrameworkCore.Tools -Version 3.1.1 PM> Install-Package Microsoft.EntityFrameworkCore.SqlServer -Version 3.1.1

值得注意的是,版本号(在撰写本文时是最新的版本号)可能会发生更改:请务必在本书的 GitHub 存储库中三次检查它!

SQL Server 数据提供程序

在已安装的名称空间中,值得注意的是存在Microsoft.EntityFrameworkCore.SqlServer,它是实体框架核心的Microsoft SQL 数据库提供程序。此高度通用的连接器为整个 Microsoft SQL Server 数据库系列(包括最新的 SQL Server 2019)提供了一个接口。

DBMS 许可模型

尽管有一个相当昂贵(至少可以说)的许可模式,但至少有三个 Microsoft SQL 版本可以免费使用,只要满足某些要求:

  • 评估版是免费的,但没有生产使用权,这意味着我们只能在开发服务器上使用。此外,它只能使用 180 天。之后,我们必须购买许可证或卸载它(并迁移到其他版本)。
  • 开发者版也是免费的,没有生产使用权。但是,如果我们只在开发和/或测试场景中使用它,则可以不受限制地使用它。
  • Express Edition是免费的,可以在任何环境中使用,这意味着我们可以在开发和生产服务器上使用它。但是,它有一些主要的性能和大小限制,可能会阻碍复杂和/或高流量 web 应用的性能。

For additional information regarding the various SQL Server editions, including the commercial ones that do require a paid licensing model, check out the following links:

https://www.microsoft.com/en-us/sql-server/sql-server-2019 https://www.microsoft.com/en-us/sql-server/sql-server-2019-comparison

正如我们很容易看到的,开发者Express版本对于像我们在本书中所玩的那些小型 web 应用来说都是非常有用的。

Linux 呢?

SQL Server 2019 也可用于 Linux,并正式支持以下发行版:

  • 红帽企业RHEL
  • SUSE 企业服务器
  • Ubuntu

除此之外,它还可以设置为在 Docker 上运行,甚至在 Azure 上配置为虚拟机,如果我们不想安装本地 DMBS 实例并节省宝贵的硬件资源,这通常是一个很好的选择。

至于许可模式,对于所有这些环境,所有 SQL Server 产品的许可方式都是相同的:这基本上意味着我们可以在我们选择的平台上使用我们的许可证(包括免费许可证)。

SQL Server 替代方案

如果您不想使用 Microsoft SQL Server,您可以 100%自由选择其他 DBMS 引擎,如 MySQL、PostgreSQL 或任何其他产品,只要它得到某种实体框架官方(或第三方)支持。

我们应该现在作出决定吗?这完全取决于我们想要采用的数据建模方法;目前,为了简单起见,我们将坚持使用 Microsoft SQL Server 系列,它允许我们在本地计算机(开发和/或生产)或 Azure(由于其 200 欧元的成本和 12 个月的免费试用期)上免费安装一个像样的 DBMS:现在不用担心,我们稍后会实现的。

数据建模方法

现在我们已经安装了实体框架,并且我们或多或少地知道我们将使用哪种 DBMS,我们必须在三种可用方法中选择一种来建模数据结构:模型优先数据库优先代码优先。每一种方法都有其相当多的优点和缺点,有经验的人和经验丰富的.NET 开发人员几乎肯定都知道这一点。尽管我们不会深入研究这些问题,但在做出选择之前对每一个问题进行简要总结可能是有用的。

模型优先

如果您不熟悉 Visual Studio IDE 设计工具,例如基于 XML 的数据集模式(XSD)和实体设计器模型 XML 可视化界面EDMX),那么模型优先的方法可能会相当混乱。理解它的关键是要承认这样一个事实,即这里的单词 Model 是用来定义用设计工具构建的可视化图表。然后,框架将使用该图自动生成 SQL 脚本和数据模型源代码文件。

总之,我们可以说,先建立模型意味着处理可视化的 EDMX 图,并让实体框架相应地创建/更新其余的

下面几节将解释其利弊。

赞成的意见

这种方法有以下好处:

  • 我们将能够使用可视化设计工具创建数据库模式和类图作为一个整体,这在数据结构非常大的情况下非常有用。
  • 每当数据库发生更改时,可以相应地更新模型,而不会丢失数据。

欺骗

然而,也有一些不利因素,如下所示:

  • 图表驱动、自动生成的 SQL 脚本在更新时可能导致数据丢失。一个简单的解决方法是在磁盘上生成脚本并手动修改它们,这将需要良好的 SQL 知识。
  • 处理图表可能很棘手,特别是如果我们想精确控制我们的模型类;我们并不总是能够得到我们想要的,因为实际的源代码将由工具自动生成。

数据库优先

考虑到 Model First 的缺点,我们可以认为数据库优先可能是一种方式。如果我们已经有了一个数据库,或者不介意事先建立它,这可能是真的。在这种情况下,数据库优先的方法与模型优先的方法相似,只是它的方向相反;我们没有手动设计 EDMX 并生成 SQL 脚本来创建数据库,而是构建后者,然后使用 Entity Framework designer 工具生成前者。

我们可以这样概括,首先进入数据库意味着构建数据库,并让实体框架相应地创建/更新其余的

下面几节将解释其利弊。

赞成的意见

以下是数据库优先方法的主要优点:

  • 如果我们有一个已经存在的数据库,这可能是一个可行的方法,因为它将使我们无需重新创建它。
  • 数据丢失的风险将保持在最低限度,因为任何结构更改或数据库模型更新都将始终在数据库本身上执行。

欺骗

以下是缺点:

  • 如果我们要处理集群、多个实例或许多开发/测试/生产环境,手动更新数据库可能会很棘手,因为我们必须手动保持它们的同步,而不是依赖代码驱动的更新/迁移或自动生成的 SQL 脚本。
  • 与使用模型优先方法相比,我们对自动生成的模型类(及其源代码)的控制更少。这需要对环境足迹公约和标准有广泛的了解;否则,我们将常常难以得到我们想要的东西。

代码优先

最后但并非最不重要的是 Entity Framework 自版本 4 以来的旗舰方法,它支持优雅、高效的数据模型开发工作流。这种方法的吸引力很容易在其前提中找到;代码优先方法允许开发人员仅使用标准类定义模型对象,而不需要任何设计工具、XML 映射文件或繁琐的自动生成代码。

总之,我们可以说,先编写代码意味着编写我们将在项目中使用的数据模型实体类,并让实体框架相应地生成数据库

下面几节将解释其利弊。

赞成的意见

以下是这种方法的优点:

  • 不需要任何图表和可视化工具,这对于中小型项目非常有用,因为这样可以节省大量时间。
  • 它有一个流畅的代码 API,允许开发人员遵循约定而不是配置的方法,以便它可以处理最常见的场景,同时也让他们有机会切换到自定义的、基于属性的实现,以覆盖自定义数据库映射的需要。

欺骗

以下是这种方法的缺点:

  • 需要对 C#和更新的 EF 公约有良好的了解。
  • 维护数据库通常很棘手,处理更新时也不会丢失数据。迁移支持是在 4.3 中添加的,用于克服此问题,并从那时起不断更新,极大地缓解了此问题,尽管它也以负面方式影响了学习曲线。

作出选择

考虑到这三个选项的优缺点,没有一个整体的更好的最好的方法;相反,我们可以说每个项目场景都可能有一个最适合的方法。

关于我们的项目,考虑到我们还没有数据库,并且我们的目标是一个灵活、可变的小规模数据结构,采用代码优先的方法可能是一个不错的选择。

然而,要做到这一点,我们需要创建一些实体,并找到一个合适的 DBMS 来存储数据:这正是我们在下面几节要做的。

创建实体

现在我们有了一个数据源,我们可以利用前面提到的代码优先方法的一个主要优点,尽早开始编写实体类,而不用太担心最终将使用什么数据库引擎。

说实话,我们已经知道一些我们最终会用到的东西。我们不会采用 NoSQL 解决方案,因为它们还没有得到实体框架核心的正式支持;我们也不想承诺购买昂贵的许可计划,因此 Oracle 和 SQL Server 的商业版可能也不在考虑之列。

这就给我们留下了相对较少的选择:SQLServerDeveloper(或 Express)版、MySQL/MariaDB 或其他不太知名的解决方案,如 PostgreSQL。此外,我们仍然不能 100%确定是否在开发机器(和/或生产服务器)上安装本地 DBMS 实例,或者是否依赖于云托管解决方案(如 Azure)。

也就是说,首先采用代码将使我们有机会推迟调用,直到数据模型就绪。

然而,要创建实体类,我们需要知道它们将包含什么类型的数据以及如何构造它:这在很大程度上取决于我们最终希望首先使用代码创建的数据源和数据库表。

在以下部分中,我们将学习如何处理这些任务。

定义实体

在实体框架中,以及在大多数 ORM 框架中,实体是映射到给定数据库表的类。实体的主要目的是使我们能够以面向对象的方式处理数据,同时使用强类型属性访问每行的表列(和数据关系)。我们将使用实体从数据库中获取数据,并将它们序列化为用于前端的 JSON。我们也会做相反的事情,也就是说,每当前端发出我们需要持久化数据库的状态时,从 POST 数据反序列化它们。

如果我们试着扩大我们的关注点,看看总体情况,我们将能够看到实体如何在 DBMS、web 应用的后端前端部分之间的整个双向数据流中发挥核心作用。

为了理解这样一个概念,我们来看看下面的图表:

我们可以清楚地看到,实体框架核心的主要目的是将数据库表映射到实体类:这正是我们现在需要做的。

解压我们刚才下载的 world cities 压缩文件,打开worldcities.xlsx文件:如果您没有 MS Excel,可以使用 Google Sheets 在 Google Drive 上导入,如下 URL 所示:http://bit.ly/worldcities-xlsx

Right after importing it, I also took the chance to make some small readability improvements to that file: bold column names, resizing the columns, changing the background color and freezing on the first row, and so on.

如果打开前面的 URL,我们将看到导入的电子表格的外观:

通过查看电子表格标题,我们可以推断出至少需要两个数据库表:

  • 城市:对于ABCD(如果我们想保留这些唯一 ID,可以说是K
  • 国家EFG

从常识来看,这似乎是最方便的选择。或者,我们可以将所有内容都放在一个Cities表中,但我们会有大量冗余内容,这是我们可能希望避免的。

如果我们要处理两个数据库表,这意味着我们首先需要两个实体来映射它们并创建它们,因为我们计划采用代码优先的方法。

城市实体

让我们从City实体开始。

从项目的解决方案资源管理器中,执行以下操作:

  1. WorldCities项目的根级新建/Data/文件夹;这将是我们所有实体框架相关类将驻留的地方。
  2. 创建一个/Data/Models/文件夹。
  3. 添加一个新的 ASP.NET Core|代码|类文件,将其命名为City.cs,并用以下代码替换示例代码:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;

namespace WorldCities.Data.Models
{
    public class City
    {
       #region Constructor
        public City()
        {

        }
        #endregion

        #region Properties
        /// <summary>
        /// The unique id and primary key for this City
        /// </summary>
        [Key]
        [Required]
        public int Id { get; set; }

        /// <summary>
        /// City name (in UTF8 format)
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// City name (in ASCII format)
        /// </summary>
        public string Name_ASCII { get; set; }

        /// <summary>
        /// City latitude
        /// </summary>
        public decimal Lat { get; set; }

        /// <summary>
        /// City longitude
        /// </summary>
        public decimal Lon { get; set; }
        #endregion

        /// <summary>
        /// Country Id (foreign key)
        /// </summary>
        public int CountryId { get; set; }
    }
}

正如我们所见,我们为我们早期识别的每个电子表格列添加了一个专用属性;我们还包括一个CountryId属性,我们将使用该属性映射与城市相关的Country外键(稍后将对此进行详细介绍)。我们还试图通过为每个属性提供一些有用的注释来提高实体类源代码的整体可读性,这些注释肯定会帮助我们记住它们的用途。

最后但并非最不重要的一点是,值得注意的是,我们利用一些数据注释属性来修饰实体类,因为它们是覆盖默认代码优先约定的最方便的方式。更具体地说,我们使用了以下注释:

  • [Required]:将属性定义为必填(不可空)字段。
  • [Key]:表示该属性承载数据库表的主键
  • [ForeignKey]:表示该属性承载一个外部表的

那些对实体框架(和关系数据库)有一定经验的人很可能会理解这些数据注释的用途:它们是一种方便的方法,可以指导实体框架在使用代码优先方法时如何正确构建数据库。这里没有什么复杂的东西;我们只是告诉 Entity Framework,为承载这些属性而创建的数据库列应该根据需要进行设置,主键应该以一对多的关系绑定到不同表中的其他外部列。

The binding that's declared using the [ForeignKey] Data Annotation will be formally enforced by creating a constraint, as long as the DB engine supports such a feature.

为了使用数据注释,我们必须在类的开头添加对System.ComponentModel.DataAnnotationsSystem.ComponentModel.DataAnnotations.Schema名称空间的引用。如果我们看一看前面的代码,就会发现这两个名称空间都被 using 语句引用了。

If you want to find out more about Data Annotations in Entity Framework Core, we strongly suggest reading the official documentation, which can be found at the following URL: https://docs.efproject.net/en/latest/modeling/index.html.

下一个实体将是识别国家的实体,该实体将与Cities建立一对多关系。

This is hardly a surprise: we're definitely going to expect a single country for each City and multiple Cities for each given Country: this is what one-to-many relationships are for.

右键点击/Data/Models/文件夹,添加Country.cs类文件,并填入以下代码:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Threading.Tasks;

namespace WorldCities.Data.Models
{
    public class Country
    {
        #region Constructor
        public Country()
        {

        }
        #endregion

        #region Properties
        /// <summary>
        /// The unique id and primary key for this Country
        /// </summary>
        [Key]
        [Required]
        public int Id { get; set; }

        /// <summary>
        /// Country name (in UTF8 format)
        /// </summary>
        public string Name { get; set; }

        /// <summary>
        /// Country code (in ISO 3166-1 ALPHA-2 format)
        /// </summary>
        public string ISO2 { get; set; }

        /// <summary>
        /// Country code (in ISO 3166-1 ALPHA-3 format)
        /// </summary>
        public string ISO3 { get; set; }
        #endregion
    }
}

同样,每个电子表格列都有一个属性,带有相关的数据注释和注释。

ISO 3166 is a standard that was published by the International Organization for Standardization (ISO) that's used to define unique codes for the names of countries, dependent territories, provinces, and states. For additional information, check out the following URLs:

https://en.wikipedia.org/wiki/ISO_3166 https://www.iso.org/iso-3166-country-codes.html

The part that describes the country codes is the first one (ISO 3166-1), which defines three possible formats: ISO 3166-1 alpha-2 (two-letter country codes), ISO 3166-1 alpha-3 (three-letter country codes), and ISO 3166-1 numeric (three-digit country codes). For additional information about the ISO 3166-1 ALPHA-2 and ISO 3166-1 ALPHA-3 formats, which are the ones that are used in our data source and therefore in this book, check out the following URLs:

https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 https://en.wikipedia.org/wiki/ISO_3166-1_alpha-3

定义关系

现在,我们已经构建了主要的CityCountry实体框架,我们需要加强我们知道它们之间存在的关系。我们希望能够做一些事情,比如检索一个Country,然后浏览到所有相关的Cities,可能是以强类型的方式。

为此,我们必须添加两个新的实体相关属性,每个实体类一个。更具体地说,我们将添加以下内容:

  • 我们的City实体类别中的Country财产,将包含一个与该城市相关的国家(即母公司
  • 我们的Country实体类中的Cities属性,将包含与该国相关的城市集合(即儿童

如果我们深入观察并尝试可视化这些实体之间的关系,我们将能够看到前一个属性如何识别(从每个子视图),而后一个属性将包含(从父视图)当前位置这种模式正是我们所期望的一对多关系,就像我们正在处理的关系。

在以下部分中,我们将学习如何实现这两个导航属性。

将 Country 属性添加到城市实体类

在文件末尾附近添加以下代码行,靠近属性区域的末尾(新行高亮显示):

using System.ComponentModel.DataAnnotations.Schema;

// ...existing code...

/// <summary>
/// Country Id (foreign key)
/// </summary>
[ForeignKey("Country")]
public int CountryId { get; set; }
#endregion

#region Navigation Properties
/// <summary>
/// The country related to this city.
/// </summary>
public virtual Country Country { get; set; }
#endregion

// ...existing code...

如我们所见,除了添加新的Country属性外,我们还使用新的[ForeignKey("Country")]数据注释装饰了现有的CountryId属性。由于该注释,Entity Framework 将知道这样一个属性将托管一个外部表的主键,Country导航属性将用于托管实体。

It's worth noting that the binding that's declared using that [ForeignKey] data annotation will be also formally enforced by creating a constraint, as long as the DB engine supports such a feature.

通过查看前面源代码的第一行可以看出,要使用[ForeignKey]数据注释,我们必须在类的开头添加对System.ComponentModel.DataAnnotations.Schema名称空间的引用。

将 Cities 属性添加到 Country 实体类

同样,在属性区域的末尾添加以下内容(新行高亮显示):

// ...existing code...

#region Navigation Properties
/// <summary>
/// A list containing all the cities related to this country.
/// </summary>
public virtual List<City> Cities { get; set; }
#endregion

// ...existing code...

就这样。如我们所见,由于一对多关系不需要来自端的外键属性,因此没有必要为该实体定义外键属性:因此,不需要添加[ForeignKey]数据注释和/或其所需的命名空间。

实体框架核心加载模式

现在我们在Country实体中有了Cities属性,在City实体中有了相应的[ForeignKey]数据注释,您可能想知道我们如何使用这些导航属性来加载相关实体。换句话说:我们将如何在需要时在国家实体内填充城市财产?

这样的问题让我们有机会花几分钟列举 Entity Framework Core 支持的三种 ORM 模式,以加载此类相关数据:

  • 急加载:作为初始查询的一部分,从数据库加载相关数据。
  • 显式加载:以后从数据库显式加载相关数据。
  • 延迟加载:第一次访问实体导航属性时,从数据库中透明加载相关数据。这是三种模式中最复杂的模式,如果没有正确实现,可能会受到一些严重的性能影响。

理解这一点很重要,无论何时我们想要加载实体的相关数据,我们都需要激活(或实现)其中一种模式。这意味着,在我们的特定场景中,Country实体的Cities属性将在我们从数据库获取一个或多个国家时设置为 NULL,除非我们明确告知实体框架核心也加载城市这是在处理 Web API 时要考虑的一个非常重要的方面,因为它肯定会影响我们的.NETCyrasyTo.T6.后端 AutoT7A.将如何服务于我们的 JSON 结构化数据响应到我们的 To8T8 前端前端 T9 角客户机。

为了理解我们的意思,让我们来看看几个例子。

下面是一个标准的实体框架核心查询,用于从给定的Id中检索Country

var country = await _context.Countries
    .FindAsync(id);

return country; // country.Cities is still set to NULL

正如我们所看到的,country变量返回给调用者,Cities属性设置为 NULL,这仅仅是因为我们没有要求它:正是因为这个原因,如果我们将该变量转换为 JSON 对象并返回给客户端,JSON 对象也将不包含任何城市。

下面是一个实体框架核心查询,使用急加载从给定id中检索country

var country = await _context.Countries
 .Include(c => C.Cities)    .FindAsync(id);

return country; // country.Cities is (eagerly) loaded

让我们试着了解一下这里发生了什么:

  • 在查询开始时指定的Include()方法告诉实体框架核心激活急切加载数据检索模式。
  • 对于新模式,EF 查询将在单个查询中获取country以及所有相应的城市。
  • 由于所有这些原因,返回的country变量的Cities属性将填充与country相关的所有cities(即CountryId值将等于国家id值)。

For additional information regarding lazy loading, eager loading, and explicit loading**, we strongly suggest that you take a look at the following URL: https://docs.microsoft.com/en-US/ef/core/querying/related-data.

这样,我们就完成了实体的处理,至少目前是这样。现在,我们只需要为自己建立一个 DBMS,这样我们就可以实际创建数据库了。

获取 SQL Server

让我们一劳永逸地缩小这一差距,并为自己提供一个 SQL Server 实例。正如我们已经提到的,我们可以采取两条主要路线:

  • 在我们的开发机器上安装本地 SQL Server 实例(Express 或 Developer Edition)。
  • 使用 Azure平台上提供的几种选项之一,在 Azure 上设置 SQL 数据库(和/或服务器)。

前一个选项体现了软件和 web 开发人员从一开始就一直使用的经典的无云方法:本地实例很容易实现,并将提供我们在开发和生产环境中需要的一切……只要我们不关心数据冗余,由于我们的服务器是一个单一的物理实体,因此基础架构负载过大,可能会影响性能(对于高流量网站)、扩展和其他瓶颈。

在 Azure 中,事情以不同的方式运行:将我们的 DBMS 放在那里让我们有机会将 SQL Server 工作负载作为托管基础设施(基础设施即服务IaaS)或托管服务(PaaS运行):如果我们想自己处理数据库维护任务,例如应用补丁和进行备份,那么第一个选项非常好;如果我们希望将此类操作委托给 Azure,则第二个选项更可取。然而,无论我们选择何种路径,我们都将拥有一个可扩展的数据库服务,它具有完全冗余和无单点故障保证,以及许多其他性能和数据安全优势。正如我们很容易猜测的那样,其负面影响如下:额外的成本以及我们将把数据放在别处这一事实,在某些情况下,这可能是隐私和数据保护方面的一个主要问题。

在下一节中,我们将快速总结如何实现这两种方法,以便做出最方便的选择。

安装 SQL Server 2019

如果我们想避开云,坚持“老派”的做法,我们可以选择在我们的开发(以及以后的生产)机器上安装一个SQL Server Express(或开发者)本地实例。

为此,请执行以下步骤:

  1. 从以下 URL 下载 SQL Server 2019 内部部署安装包(可以说是 Windows 版本,但也可以使用 Linux 安装程序):https://www.microsoft.com/en-us/sql-server/sql-server-downloads
  2. 双击可执行文件开始安装过程。当提示输入安装类型时,选择默认选项(除非我们需要配置一些高级选项以满足特定需要,前提是我们知道自己在做什么)。

然后,安装包将开始下载所需的文件。完成后,我们只需单击 New SQL Server 单机安装(从顶部开始的第一个可用选项,如以下屏幕截图所示)即可开始实际的安装过程:

接受许可条款并继续,保留所有默认选项,并在要求时执行所需操作(例如打开Windows 防火墙

If we want to keep our disk space consumption to a minimum amount, we can safely remove the SQL Replication and Machine Learning services from the Feature Selection section and save roughly 500 GB.

将实例名称设置为SQLExpress,实例 ID 设置为SQLEXPRESS。记住这个选择:当我们必须写下连接字符串时,我们将需要它。

当要求我们选择身份验证模式时(如下面的屏幕截图所示),请选择以下选项之一:

  • Windows 身份验证模式,如果我们希望能够仅从本地计算机(使用 Windows 凭据)无限制地访问数据库引擎
  • 混合模式,启用 SQL Server 系统管理员(即sa用户)并为其设置密码

在以下屏幕截图中可以看到这两个选项:

前一个选项对于安全性来说是非常好的,而后一个选项则更加通用,特别是如果我们要使用 SQL server 内置的管理界面远程管理 SQL server,这是我们将用来创建数据库的工具。

Those who need a more comprehensive guide to perform the SQL Server local instance installation can take a look at the following tutorials:

Installing SQL Server on Windowshttps://docs.microsoft.com/en-US/sql/database-engine/install-windows/installation-for-sql-server.

Installing SQL Server on Linuxhttps://docs.microsoft.com/en-US/sql/linux/sql-server-linux-setup.

SQL Server 安装完成后,我们还应该安装SQL Server 管理工具——这是一组有用的工具,可以用来管理本地和/或远程可用的任何 SQL 实例,只要服务器可以访问并且已配置为允许远程访问。更具体地说,我们需要的工具是SQL Server Management StudioSSMS),它基本上是一个 GUI 界面,可用于创建数据库、表、存储过程等,以及操作数据。

Although being available from the SQL Server installation and setup tool, SSMS is a separate product and is available (free of charge) at the following URL: https://docs.microsoft.com/en-us/sql/ssms/download-sql-server-management-studio-ssms.

然而,在使用它之前,我们将花一些宝贵的时间讨论 Azure 路径。

在 Azure 上创建 SQL 数据库

如果您想摆脱 DBMS 本地实例,采用Azure路线,我们的待办事项列表完全取决于我们将从 Azure 平台提供的主要方法中选择哪一种。以下是终端用户可以选择的三个主要选项,从最低到最昂贵。详情如下:

  • SQL 数据库:这是一个基于 SQL Server 企业版的全管理 SQL 数据库引擎。此选项允许我们使用平台即服务PaaS)使用和计费模型来设置和管理托管在 Azure 云中的一个或多个单一关系数据库:更具体地说,我们可以将其定义为数据库即服务DBaaS方法。此选项提供了内置的高可用性、智能性和管理功能,这意味着它非常适合那些需要多功能解决方案的人,而无需配置、管理和支付整个服务器主机的费用。
  • SQL 托管实例:这是 Azure 上的一个专用 SQL 托管实例,这是一个可扩展的数据库服务,与标准 SQL Server 实例几乎 100%兼容,并具有 IaaS 使用和计费模型。此选项提供了前一个实例(SQL 数据库)的所有 PaaS 好处但添加了一些与基础设施相关的附加功能,如本机虚拟网络VNet)、自定义专用 IP 地址、具有共享资源的多个数据库等。
  • SQL 虚拟机:这是一个完全管理的 SQL Server,由 Windows 或 Linux 虚拟机组成,上面安装了 SQL Server 实例。这种方法还采用 IaaS 使用和计费模式,对整个 SQL Server 实例和底层操作系统提供完全的管理控制,因此是最复杂和可定制的一个。与其他两个选项(SQL 数据库和 SQL 托管实例)最显著的区别在于,SQL Server VM 还允许完全控制数据库引擎:我们可以选择何时开始维护/修补、更改恢复模型、暂停/启动服务,等等。

For more information regarding the pros and cons of the Azure options described here, we strongly suggest that you read the following guide: https://docs.microsoft.com/en-US/azure/sql-database/sql-database-paas-vs-sql-server-iaas.

所有这些选项都很好,虽然在总体成本上有很大不同,但可以免费激活:SQLDatabase可以说是最便宜的一个,因为它可以免费使用 12 个月,这要感谢 Azure 提供的试用订阅计划,只要我们将其大小保持在 250GB 以下;无论是SQL 托管实例还是SQL 虚拟机都相当昂贵,因为它们都提供了虚拟化的 IaaS,但它们可以免费激活(至少在几周内),由同一 Azure 试用订阅计划提供 200 欧元。

在以下几节中,我们将学习如何设置 SQL 数据库,因为从长远来看,这是一种成本较低的方法:唯一的缺点是我们必须将其大小保持在 250GB 以下。。。考虑到我们的世界城市数据源文件的大小小于 1GB,这绝对不是问题。

In case we want to opt for an Azure SQL managed instance (option #2), here's a great guide explaining how to do that: https://docs.microsoft.com/en-us/azure/sql-database/sql-database-managed-instance-get-started.

If you wish to set up a SQL Server installed on a virtual machine (option #3), here's a tutorial covering that topic: https://docs.microsoft.com/en-US/azure/virtual-machines/windows/sql/quickstart-sql-vm-create-portal.

设置 SQL 数据库

让我们从访问以下 URL 开始:https://azure.microsoft.com/en-us/free/services/sql-database/

这将使我们进入以下网页,该网页允许我们创建 Azure SQL 托管实例:

单击开始自由按钮并创建一个新帐户。

If you already have a valid MS account, you can definitely use it; however, you should only do that if you're sure that you want to use the free Azure trial on it: if that's not the case, consider creating a new one.

在简短的注册表单(和/或登录阶段)之后,我们将被重定向到 Azure 门户。

不言而喻,如果我们登录的帐户已经过了免费期,或者有一个有效的付费订阅计划,我们将优雅地恢复:

最终,在我们整理好所有事情之后,我们应该能够访问 Azure 门户(https://portal.azure.com )在它所有的荣耀中:

到达后,请执行以下操作:

  1. 单击“创建资源”按钮访问 Azure Marketplace。
  2. 搜索名为 Azure SQL 的条目。
  3. 单击“创建”以访问选择页面,如以下屏幕截图所示:

IMPORTANT: Be careful that you don't pick the SQL managed instance entry instead, which is the one for creating the SQL Server Virtual Machine—this is option #2 that we talked about earlier.

在前面的选择屏幕中,执行以下操作:

  1. 选择第一个选项(SQL 数据库)。
  2. 将“资源类型”下拉列表设置为“单个数据库”。
  3. 单击“创建”按钮以启动主安装向导。

在此过程中,我们还将被要求创建我们的第一个A祖尔租户(除非我们已经有了一个)。这是一个虚拟组织,拥有并管理一组特定的 Microsoft 云服务。租户由以下格式的唯一 URL 标识:<TenantName>.onmicrosoft.com。给它一个合适的名字,然后继续。

配置实例

单击“创建”按钮后,系统将要求我们使用类似向导的界面配置 SQL 数据库,该界面分为以下选项卡:

  • 基本信息:订阅类型、实例名称、管理员用户名和密码等
  • 网络:网络连接方法和防火墙规则
  • 附加设置:排序和时区 *标记:一组名称/值对,可用于将 Azure 资源逻辑组织为共享公共范围的功能类别或组(如生产和测试)。 查看+创建:查看并确认前面的所有内容*

在“基本”选项卡中,我们必须插入数据库详细信息,例如数据库名称和要使用的服务器。如果这是我们第一次来这里,我们将没有任何可用的服务器。因此,我们必须通过点击创建新链接并填写弹出表单来创建第一个,该表单将滑入屏幕最右侧。请务必设置一个非平凡的服务器管理员登录和一个复杂的密码**,因为我们将需要这些凭据来创建即将到来的连接字符串。

以下屏幕截图显示了如何配置向导此部分的示例:

“基本”选项卡中的最后一个选项将要求我们提供计算+存储类型:对于这个特定项目,我们完全可以选择最小可能的 tier a 基本存储类型,最大空间为 2 GB。

但是,如果我们想大胆一点,我们可以选择存储容量为 250 GB 的标准类型,因为它在 12 个月内仍然是免费的(请参见下面的屏幕截图):

在“网络”选项卡中,确保选择公共端点以启用来自 internet 的外部访问,以便我们能够从所有环境连接到数据库。我们还应该将防火墙规则设置为“是”,以允许 Azure 服务和资源访问服务器,并将我们当前的 IP 地址添加到允许的 IP 白名单中。

Wait a minute: isn't that a major security issue? What if our databases contain personal or sensitive data?

As a matter of fact, it actually is: allowing public access from the internet is something we should always avoid unless we're playing with open data for testing, demonstrative, or tutorial purposes... which is precisely what we're doing right now.

附加设置和标记选项卡与默认设置一致:只有在需要更改某些选项(如最适合我们的语言和国家的排序规则和时区)或激活特定内容(如高级数据安全时,我们才应更改它们-这对于我们当前的需求来说是完全不必要的。

在“查看+创建”选项卡中,我们将有最后一次机会查看和更改设置(如以下屏幕截图所示):如果我们不确定这些设置,我们将有机会返回并更改它们。当我们 100%确定后,我们可以点击“创建”按钮,在几秒钟内部署 SQL 数据库:

It's worth noticing that we can also Download a template for automation, in case we want to save these settings to create additional SQL Databases in the future.

就是这样:现在,我们可以集中精力配置数据库。

配置数据库

不管路径如何,我们都会选择本地实例或 Azure,我们应该准备好管理新创建的 Azure SQL 数据库。

最实用的方法是使用 SSMS,这是一种免费的 SQL Server 管理 GUI,我们可以按照前面解释的说明免费下载(请参见安装 SQL Server 2019部分)。如果我们还没有安装它,我们可以在下载后立即安装。

完成后,我们只需选择SQL Server Authentication,然后输入我们在 Azure 上创建 SQL 数据库时选择的服务器名称、登录名、和密码。这可以在以下屏幕截图中看到:

**

通过单击“连接”按钮,我们应该能够登录到我们的数据库服务器。一旦 SSMS 连接到 SQL 数据库服务器,就会出现一个服务器资源管理器窗口,其中包含一个表示 SQL server 实例结构的树视图。这是我们用来创建数据库的界面,也是我们的应用用来访问数据库的用户/密码。

创建世界城市数据库

如果我们采用 Azure SQL 数据库路由,我们应该已经能够在左侧对象浏览器树的Databases文件夹中看到WorldCities数据库:

*

或者,如果我们安装了本地的SQL Server Express开发实例,我们必须通过执行以下操作手动创建它:

  1. 右键点击Databases文件夹。 从上下文菜单中选择“添加数据库”。 键入 WorldCities 名称,然后单击 OK 创建它。

一旦创建了数据库,我们将有机会通过点击左侧的加号(+符号来扩展其树节点,并通过 SSMS GUI 与所有子对象存储过程用户等进行可视化交互。不用说,如果我们现在这样做,我们将找不到表,因为我们还没有创建它们:这是实体框架稍后将为我们做的事情。然而,在此之前,我们将添加一个登录**帐户,使我们的 web 应用能够连接。

添加 WorldCities 登录

返回根目录Databases文件夹,展开位于其下方的Security文件夹。到达后,请执行以下操作:****

* 右键点击Logins子文件夹,选择新建登录。 * 在出现的模式窗口中,将登录名设置为WorldCities。 * 从登录名下方的单选按钮列表中,选择 SQL Server Authentication,并设置一个强度合适的密码(例如 MyVeryOwn$721,从现在起,我们将在代码示例和屏幕截图中使用此密码)。 * 确保禁用用户下次登录时必须更改密码选项(默认为勾选;否则,Entity Framework Core 稍后将无法执行登录。 * 将用户的默认数据库设置为WorldCities。 * 查看所有选项,然后单击“确定”创建WorldCities帐户。

If we want a simpler password, such as WorldCities or Password, we might have to disable the enforce password policy option. However, we strongly advise against doing that: choosing a weak password is never a wise choice, especially if we do that in a production-ready environment. We suggest that you always use a strong password, even in testing and development environments. Just be sure not to forget it, as we're going to need it later on.

将登录名映射到数据库

我们需要做的下一件事是将这个登录正确地映射到我们前面添加的WorldCities数据库。以下是如何做到这一点:

  1. 双击 Databases | Security 文件夹中的WorldCities登录名,打开我们几秒钟前使用的相同模式。
  2. 从左侧的导航菜单切换到用户映射选项卡。
  3. 点击WorldCities数据库右侧的复选框:用户单元格应自动填入WorldCities值。如果没有,我们需要手动输入WorldCities
  4. 在右下面板的数据库角色成员身份框中,分配db_owner成员身份角色。

**以下屏幕截图描述了上述所有步骤:

就这样!现在,我们可以回到 web 应用项目,添加连接字符串,并使用实体框架代码优先的方法创建表(和数据)。

首先使用代码创建数据库

在继续之前,让我们做一个快速检查表:

  • 我们的实体结束了吗?
  • 我们有 DBMS 和WorldCities数据库吗?
  • 我们是否已经完成了所有需要完成的步骤,以便首先使用代码实际创建和填写上述数据库?

事实上,我们还需要注意两件事:

  • 设置适当的数据库上下文
  • 在我们的项目中启用代码优先数据迁移支持

在以下部分中,我们将填补所有这些空白,并最终填补我们的WorldCities数据库。

设置 DbContext

为了将数据作为对象/实体类进行交互,实体框架核心使用Microsoft.EntityFrameworkCore.DbContext类,也称为DbContext或简称上下文。此类负责运行时期间的所有实体对象,包括使用数据库中的数据填充它们、跟踪更改以及在 CRUD 操作期间将它们持久化到数据库。

我们可以很容易地为我们的项目创建我们自己的DbContext类,我们称之为ApplicationDbContext——通过执行以下操作:

  1. 在解决方案资源管理器中,右键单击我们刚才创建的/Data/文件夹,并添加一个新的ApplicationDbContext.cs类文件。
  2. 用以下代码填充它:
using Microsoft.EntityFrameworkCore;
using WorldCities.Data.Models;

namespace WorldCities.Data
{
    public class ApplicationDbContext : DbContext
    {
        #region Constructor
        public ApplicationDbContext() : base()
        {
        }

        public ApplicationDbContext(DbContextOptions options) 
         : base(options)
        {
        }
        #endregion Constructor

        #region Methods
        protected override void OnModelCreating(ModelBuilder 
         modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // Map Entity names to DB Table names
            modelBuilder.Entity<City>().ToTable("Cities");
         modelBuilder.Entity<Country>().ToTable("Countries");
        }
        #endregion Methods

        #region Properties
        public DbSet<City> Cities { get; set; }
        public DbSet<Country> Countries { get; set; }
        #endregion Properties
    }
}

我们在这里做了几件重要的事情:

  • 我们重写了OnModelCreating方法来手动定义实体类的数据模型关系。注意,我们使用modelBuilder.Entity<TEntityType>().ToTable方法手动配置了每个实体的表名;我们这样做的唯一目的是向您展示定制首先生成的数据库代码是多么容易。
  • 我们为每个实体添加了一个DbSet<T>属性,以便以后可以轻松访问它们。

数据库初始化策略

第一次创建数据库并不是我们唯一需要担心的事情;例如,我们如何跟踪数据模型肯定会发生的更改?

在以前的 EF 非核心版本(高达 6.x)中,我们可以选择代码优先方法提供的一种数据库管理模式(称为数据库初始化器数据库初始化器,也就是说,根据我们的具体需求选择适当的数据库初始化策略:CreateDatabaseIfNotExistsDropCreateDatabaseIfModelChangesDropCreateDatabaseAlwaysMigrateDatabaseToLatestVersion。此外,如果我们需要满足特定的要求,我们还可以通过扩展前面的一种方法并覆盖其核心方法来设置我们自己的自定义初始值设定项。

DbInitializer 的主要缺陷是,对于普通开发人员来说,它们没有足够的即时性和流线型。它们是可行的,但如果没有对实体框架逻辑的广泛了解,就很难处理它们。

在实体框架内核中,该模式被大大简化;没有 DBInitializer,自动数据迁移也已删除。数据库初始化方面现在完全通过 PowerShell 命令处理,唯一的例外是可以直接放置在DbContext实现构造函数上的一小部分命令,以部分自动化过程;详情如下:

  • Database.EnsureCreated()
  • Database.EnsureDeleted()
  • Database.Migrate()

目前无法通过编程方式创建数据迁移;它们必须通过 PowerShell 添加,我们很快就会看到。

更新 appsettings.json 文件

在解决方案资源管理器中,打开appsettings.json文件,并在"Logging"文件的正下方添加以下"ConnectionStrings"JSON 属性部分(新行高亮显示):

{
"ConnectionStrings": {
 "DefaultConnection": "Server=localhost\\SQLEXPRESS; Database=WorldCities;
    User Id=WorldCities;Password=MyVeryOwn$721;
    Integrated Security=False;MultipleActiveResultSets=True" },
  "Logging": {
    "LogLevel": {
      "Default": "Warning"
    }
  },
  "AllowedHosts": "*"
}

Unfortunately, JSON doesn't support LF/CR, so we'll need to put the DefaultConnection value on a single line. If you copy and paste the preceding text, ensure that Visual Studio doesn't automatically add additional double quotes and/or escape characters to these lines; otherwise, your connection string won't work.

这是我们稍后将在项目的Startup.cs文件中引用的连接字符串。

创建数据库

现在我们已经建立了自己的DbContext并定义了一个指向WorldCities数据库的有效连接字符串,我们可以轻松地添加初始迁移并创建数据库。

更新 Startup.cs

我们要做的第一件事是将EntityFramework支持和ApplicationDbContext实现添加到我们的应用启动类中。打开Startup.cs文件,按以下方式更新ConfigureServices方法(新行高亮显示):

// ...existing code...

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews();
    // In production, the Angular files will be served 
    // from this directory
    services.AddSpaStaticFiles(configuration =>
    {
        configuration.RootPath = "ClientApp/dist";
    });

    // Add EntityFramework support for SqlServer.
    services.AddEntityFrameworkSqlServer();

 // Add ApplicationDbContext.
 services.AddDbContext<ApplicationDbContext>(options =>
 options.UseSqlServer(
 Configuration.GetConnectionString("DefaultConnection")
 )
 );
}

// ...existing code...

新代码还需要以下命名空间引用:

using Microsoft.EntityFrameworkCore;
using WorldCities.Data;

添加初始迁移

打开 PowerShell 命令提示符并浏览项目的根文件夹,在我们的示例中如下所示:

C:\ThisBook\Chapter_04\WorldCities\

到达后,键入以下命令以全局安装dotnet-ef命令行工具:

dotnet tool install --global dotnet-ef

等待安装完成。当我们收到绿色消息输出时,输入以下命令添加第一次迁移:

dotnet ef migrations add "Initial" -o "Data\Migrations" 

可选-o参数可用于更改迁移代码生成文件的创建位置;如果我们没有指定,默认情况下会创建并使用根级别的/Migrations/文件夹。因为我们将所有的EntityFrameworkCore类都放在/Data/文件夹中,所以建议也将迁移存储在那里。

上述命令将产生以下输出:

嘿,等一下:那些黄色警告信息是什么

让我们花几秒钟仔细阅读它们,并承认它们所指的问题。City实体中的 Lat/Lon 属性(都是十进制类型)显然都缺少一个明确的精度值:如果我们不提供这样的信息,entity Framework 将不知道为这些属性创建的数据库表列设置哪个精度,并将返回其默认值。如果我们的实际数据有更多的小数,那么这种回退可能会导致精度损失。

即使在我们的特定场景中,我们不能不关心这些 Lat/Lon 坐标的精度,因为我们只是在玩数据游戏,一旦我们看到这些问题,就立即修复它们绝对是明智的。幸运的是,这可以通过向这些属性添加一些数据注释轻松完成。

打开/Data/Models/City.cs文件并相应更改以下代码(修改的行突出显示):

// ...existing code...

/// <summary>
/// City latitude
/// </summary>
[Column(TypeName = "decimal(7,4)")]
public decimal Lat { get; set; }

/// <summary>
/// City longitude
/// </summary>
[Column(TypeName = "decimal(7,4)")]
public decimal Lon { get; set; }

// ...existing code...

完成后,删除/Data/Models/Migration文件夹(以及其中的所有文件),并再次启动dotnet-ef命令:

dotnet ef migrations add "Initial" -o "Data\Migrations"

这一次,迁移应该在没有黄色警告问题的情况下创建,如下面的屏幕截图所示:

这意味着我们终于有了绿灯来应用它。

If we go back to Visual Studio and take a look at our project's Solution Explorer, we will see that there's a new /Data/Migrations/ folder containing a bunch of code-generated files. Those files contain the actual low-level SQL commands that will be used by Entity Framework Core to create and/or update the database schema.

更新数据库

应用数据迁移基本上意味着创建(或更新)数据库,以便将其内容(表结构、约束等)与DbContext中的总体模式和定义以及各种实体类中的数据注释定义的规则同步。更具体地说,第一次数据迁移从头开始创建整个数据库,而随后的迁移将更新它(创建表、添加/修改/删除表字段等)。

在我们的特定场景中,我们将执行第一次迁移。下面是我们需要从命令行(在项目根文件夹中,就像以前一样)键入的一行代码:

dotnet ef database update

一旦我们点击回车,我们的命令行终端窗口的输出中将填充一堆 SQL 语句。完成后,如果一切正常,我们可以返回 SSMS 工具,刷新 Server Object Explorer 树视图,并验证WorldCities数据库以及所有相关表是否已创建:

Those of you who have used migrations before might be asking why we didn't use Visual Studio's Package Manager Console to execute these commands. The reason is simple—unfortunately, doing this won't work because the commands need to be executed within the project root folder, which is not where the Package Manager Console commands are executed. It is unknown whether that behavior will change in the near future. Until it does, we'll have to use the command line.

“找不到与命令 dotnet ef 匹配的可执行文件”错误

在撰写本文时,有一个棘手的问题影响了大多数基于.NET Core 的 Visual Studio 项目,它可能会阻止dotnet ef命令正常工作。更具体地说,在尝试执行任何基于dotnet ef的命令时,我们可能会收到以下错误消息:

No executable found matching command "dotnet-ef"

如果我们碰巧遇到此问题,我们可以尝试检查以下内容:

  • 再次检查我们是否正确添加了Microsoft.EntityFrameworkCore.ToolsMicrosoft.EntityFrameworkCore.Tools.DotNet包库(如前所述),因为它们是命令工作所必需的。
  • 确保我们在项目的根文件夹中发出的dotnet ef命令与包含<ProjectName>.csproj文件的根文件夹相同;它在其他任何地方都不起作用。

如果这两个检查都符合要求,我们可以尝试以下解决方法:右键单击项目的根文件夹,选择Edit <ProjectName>.csproj打开该文件,以便在 Visual Studio 中对其进行编辑,并查找以下元素:

<ItemGroup>
 <DotNetCliToolReference Include="Microsoft.EntityFrameworkCore.Tools"
  />
 <DotNetCliToolReference 
  Include="Microsoft.EntityFrameworkCore.Tools.DotNet" /> 
</ItemGroup>

Alternatively, we can also edit the <ProjectName>.csproj file with a text editor such as Notepad++; just ensure that you reload the project when you're done.

<ItemGroup>元素在这里只是一个容器;我们需要寻找突出显示的行(它们可能有版本属性,也可能没有,这取决于我们使用的实体框架核心版本)。

如果这些行不存在,这就是dotnet ef命令不起作用的原因。我们需要通过卸载/重新安装相关的 NuGet 软件包或手动将其添加到项目配置文件来修复这种不必要的行为。如果我们选择手动执行,我们需要确保将它们包装在新的或现有的<ItemGroup>块中。

在修复项目配置文件之后,我们可以重新启动 Visual Studio(或重新加载项目),并尝试从项目的根文件夹再次执行dotnet ef命令。在不太可能的情况下,我们最终会遇到一些 NuGet 包冲突,我们可以尝试发出dotnet update命令来修复它们,再次重新加载我们的项目,然后再次尝试执行dotnet ef命令。

A lot more can be said regarding this issue, but doing is outside the scope of this book. Those of you who want to know more can take a look at this article I wrote about it while working on my ASP.NET Core 2 and Angular 5 book at https://goo.gl/Ki6mdb.

了解迁移

在我们继续之前,先说几句话来解释什么是代码优先迁移,以及我们通过使用它们所获得的优势是很有用的。

每当我们开发一个应用并定义一个数据模型时,我们都可以确信它会因为许多好的原因发生多次更改:来自产品所有者的新需求、优化过程、整合阶段等等。将添加、删除一组属性,或更改其类型。很可能,我们迟早也会根据不断变化的需求添加新的实体和/或改变它们的关系模式。

每次我们这样做时,我们也会使数据模型与其底层的、代码优先生成的数据库不同步。当我们在开发环境中调试应用时,这不会是一个问题,因为该场景通常允许我们在项目更改时从头开始重新创建数据库。

在将应用部署到生产环境中时,我们将面临一个完全不同的情况:只要我们处理真实数据,删除和重新创建数据库就不再是一种选择。这就是代码优先迁移特性要解决的问题:让开发人员有机会更改数据库模式,而不必删除/重新创建整个数据库。

我们不会深入探讨这个话题;实体框架核心是它自己的世界,详细描述它超出了本书的范围。如果您想了解更多信息,我们建议您从的官方实体框架核心 MS 文档开始 https://docs.microsoft.com/en-us/ef/core/

是否需要数据迁移?

数据迁移可能非常有用,但它不是必需的功能,如果我们不想,我们肯定不会被迫使用它。事实上,对于许多开发人员来说,这是一个很难理解的概念,尤其是对于那些对 DBMS 设计和/或脚本编写不太了解的开发人员。在大多数情况下,管理 DBA 也可能非常复杂,例如,在公司中,DBA 角色由 It 开发团队下面的人员(如外部 It 顾问或专家)担任。

无论何时我们从一开始就不想使用它们,或者到了不想再使用它们的地步,我们都可以切换到数据库优先的方法,并开始手动设计、创建和/或修改我们的表:Entity Framework core 将非常有效,只要实体中定义的属性类型 100%匹配相应的 DB 表字段。我们完全可以做到这一点,即使是将本书中介绍的项目样本付诸实践(包括WorldCities项目,因此从现在开始,字面上就是这样),只要我们觉得我们在生活中不需要这样的技术。

或者,我们可以尝试一下,看看情况如何。一如既往,选择权在你。

填充数据库

现在我们有了一个可用的 SQL 数据库和一个DbContext可以用来读写它,我们终于准备好用我们的世界城市数据填充这些表了。

为此,我们需要实施数据播种策略。我们可以使用各种实体框架核心支持的方法之一来实现这一点:

  • 模型数据种子
  • 手工迁移定制
  • 自定义初始化逻辑

这三种方法在下面的文章中有很好的解释,以及它们各自的优缺点:https://docs.microsoft.com/en-us/ef/core/modeling/data-seeding

由于我们必须处理一个相对较大的 Excel 文件,我们将采用我们可以使用的最可定制的模式:一些自定义初始化逻辑,它将依赖于一个专用的.NET Core 控制器,我们可以在需要为数据库种子时手动甚至自动执行。

实现种子控制器

我们的自定义初始化逻辑实现将依赖于一个全新的专用控制器,称为SeedController

从我们项目的解决方案资源管理器中,执行以下操作:

  1. 右键点击/Controllers/文件夹。
  2. 单击添加|控制器。
  3. 选择API Controller - Empty选项(写作时顶部第三个选项)。
  4. 为控制器指定SeedController名称,然后单击“添加”以创建它。

完成后,打开新创建的/Controllers/SeedController.cs文件并查看源代码:您将看到只有一个空类:正如预期的空控制器一样!这很好,因为我们需要理解一些关键概念,最重要的是学习如何在源代码中正确地翻译它们。

还记得我们在Startup.cs文件中添加ApplicationDbContext类的时候吗?正如我们已经从第 2 章中了解到的,环顾,这意味着我们将实体框架核心中间件添加到了应用的管道中。这意味着我们现在可以利用.NET Core 体系结构提供的依赖项注入加载功能,在控制器中注入该DbContext类的实例。

下面是我们如何将这样一个概念转化为源代码(新行突出显示):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using WorldCities.Data;

namespace WorldCities.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class SeedController : ControllerBase
    {
 private readonly ApplicationDbContext _context;

 public SeedController(ApplicationDbContext context)
 {
 _context = context;
 }
    }
}

如我们所见,我们添加了一个_context私有变量,并使用它在构造函数中存储ApplicationDbContext类的对象实例。这样的实例将由框架通过其依赖注入特性在SeedController的构造函数方法中提供。

在充分利用DbContext实例将一组实体插入我们的数据库之前,我们需要找到一种从 Excel 文件中读取这些世界城市值的方法。我们怎么能做到呢?

导入 Excel 文件

幸运的是,有一个很棒的第三方库,它正是我们所需要的:使用 Office Open XML 格式(xlsx)读取(甚至写入!)Excel 文件,从而使其内容在任何基于.NET 的应用中都可用。

这个伟大工具的名字是EPPlus。它的作者 Jan Källman 在 GitHub 和 NuGet 上免费提供了它,网址如下:

正如我们所看到的,该项目是根据 GNU图书馆通用公共许可证LGPL)v3.0 进行许可的,这意味着我们可以无限制地将其集成到我们的软件中,只要我们不修改它。

在我们的WorldCities项目中安装EPPlus的最佳方式是使用 NuGet package manager GUI 添加 NuGet 软件包:

  1. 在项目的解决方案资源管理器中,右键单击WorldCities项目。
  2. 选择管理 NuGet 软件包。。。
  3. 使用浏览选项卡搜索EPPlus包,点击右上角的安装按钮进行安装:

**

完成后,我们可以返回到SeedController.cs文件,使用EPPlus的强大功能读取worldcities.xlsxExcel 文件。

然而,在这样做之前,明智的做法是将该文件移动到示例项目的/Data/文件夹中,这样我们就可以使用System.IO命名空间提供的.NET Core 文件系统功能来读取它。在这里,让我们创建一个/Data/Source/子文件夹,并将其与其他实体框架核心文件分开:

下面是我们需要添加到SeedController.cs文件中的源代码,以读取worldcities.xlsx文件并将所有行存储在City实体列表中:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using WorldCities.Data;
using OfficeOpenXml;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using WorldCities.Data.Models;
using System.Text.Json;

namespace WorldCities.Controllers
{
    [Route("api/[controller]/[action]")]
    [ApiController]
    public class SeedController : ControllerBase
    {
        private readonly ApplicationDbContext _context;
        private readonly IWebHostEnvironment _env;

        public SeedController(
            ApplicationDbContext context, 
            IWebHostEnvironment env)
        {
            _context = context;
            _env = env;
        }

        [HttpGet]
        public async Task<ActionResult> Import()
        {
            var path = Path.Combine(
                _env.ContentRootPath,
                String.Format("Data/Source/worldcities.xlsx"));

            using (var stream = new FileStream(
                path, 
                FileMode.Open, 
                FileAccess.Read))
            {
                using (var ep = new ExcelPackage(stream))
                {
                    // get the first worksheet

                    var ws = ep.Workbook.Worksheets[0];

                    // initialize the record counters
                    var nCountries = 0;
                    var nCities = 0;

                    #region Import all Countries
                    // create a list containing all the countries 
                    // already existing into the Database (it 
                    // will be empty on first run).
                    var lstCountries = _context.Countries.ToList();

                    // iterates through all rows, skipping the 
                    // first one
                    for (int nRow = 2;
                        nRow <= ws.Dimension.End.Row;
                        nRow++)
                    {
                        var row = ws.Cells[nRow, 1, nRow, 
                         ws.Dimension.End.Column];
                        var name = row[nRow, 5].GetValue<string>();

                        // Did we already created a country with 
                        // that name?
                        if (lstCountries.Where(c => c.Name == 
                         name).Count() == 0)
                        {
                            // create the Country entity and fill it 
                            // with xlsx data
                            var country = new Country();
                            country.Name = name;
                            country.ISO2 = row[nRow, 
                             6].GetValue<string>();
                            country.ISO3 = row[nRow, 
                             7].GetValue<string>();

                            // save it into the Database
                            _context.Countries.Add(country);
                            await _context.SaveChangesAsync();

                            // store the country to retrieve 
                            // its Id later on
                            lstCountries.Add(country);

                            // increment the counter
                            nCountries++;
                        }
                    }

                    #endregion

                    #region Import all Cities
                    // iterates through all rows, skipping the 
                    // first one
                    for (int nRow = 2;
                        nRow <= ws.Dimension.End.Row;
                        nRow++)
                    {
                        var row = ws.Cells[nRow, 1, nRow, 
                         ws.Dimension.End.Column];

                        // create the City entity and fill it 
                        // with xlsx data
                        var city = new City();
                        city.Name = row[nRow, 1].GetValue<string>();
                        city.Name_ASCII = row[nRow, 
                         2].GetValue<string>();
                        city.Lat = row[nRow, 3].GetValue<decimal>();
                        city.Lon = row[nRow, 4].GetValue<decimal>();

                        // retrieve CountryId
                        var countryName = row[nRow, 
                         5].GetValue<string>();
                        var country = lstCountries.Where(c => c.Name 
                         == countryName)
                            .FirstOrDefault();
                        city.CountryId = country.Id;

                        // save the city into the Database
                        _context.Cities.Add(city);
                        await _context.SaveChangesAsync();

                        // increment the counter
                        nCities++;
                    }
                    #endregion

                    return new JsonResult(new { 
                        Cities = nCities,
                        Countries = nCountries
                    });
                }
            }
        }
    }
}

正如你所看到的,我们在那里做了很多有趣的事情。前面的代码有很多注释,应该非常可读;然而,简要列举最相关的部分可能是有用的:

  • 我们通过依赖注入注入了一个IWebHostEnvironment实例,就像我们对ApplicationDbContext所做的一样,这样我们就可以检索 web 应用路径并能够读取 Excel 文件。
  • 我们增加了一个Import()动作方法,使用ApplicationDbContextEPPlus包读取 Excel 文件,增加CountriesCities;为了方便起见,这两项任务分为两部分。
  • 首先导入Countries,因为City实体需要CountryId外键值,当对应的Country作为新记录在数据库中创建时会返回。
  • 我们定义了一个List<Country>容器对象来存储创建后的每个Country,这样我们就可以使用 LINQ 查询该列表来检索CountryId,而不是执行大量的SELECT查询。
  • 最后但并非最不重要的一点是,我们创建了一个 JSON 对象以在屏幕上显示总体结果。

请注意,Import方法设计用于导入 230 多个国家和 12000 多个城市,因此在一台平均开发机器上,此任务可能需要 10 到 20 分钟的时间。这绝对是一个重要的数据种子!我们在强调这个框架。

In case we don't want to wait for that long, we can always give the nEndRow internal variable a fixed value, such as 1,000, to limit the total number of cities (and countries) that will be read and therefore loaded into the database.

如果我们想更仔细地了解整个导入过程是如何工作的,我们可以在if循环中放置一些断点,在它运行时检查它。

最终,我们应该能够在浏览器窗口中看到以下响应:

前面的输出表示导入已成功执行:我们做到了!我们的数据库现在充满了供我们玩的12959城市和235国家。在下一节中,我们将学习如何读取这些数据,以便能够将 Angular 引入循环。

实体控制器

现在,我们的数据库中有数千个城市和数千个国家,我们需要找到一种方法将这些数据带到 Angular,反之亦然。从第 2 章环顾我们已经知道,这个角色是由.NET 控制器扮演的,所以我们将创建两个:

*** CityController,服务(接收)城市数据 * CountryController对国家也是如此

让我们开始吧。

花旗控制器

让我们从城市开始。还记得我们创建SeedController时做了什么吗?我们现在要做的事情非常类似,但这次我们将充分利用 VisualStudio 的代码生成功能。

从我们项目的解决方案浏览器中,执行以下步骤:

  1. 右键点击/Controllers/文件夹。
  2. 单击添加|控制器。
  3. 选择 AddAPI 控制器和动作,使用实体框架选项(在撰写本文时,从顶部选择最后一个选项)。
  4. 在出现的模态窗口中,选择City模型类和ApplicationDbContext数据上下文类,如下图所示。将控制器命名为CityController并单击“添加”创建:

Visual Studio 将使用我们在此阶段指定的设置来分析我们的实体(以及我们的DbContext),并自动生成一个包含有用方法的完整 API 控制器。

以下是我们免费获得的源代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using WorldCities.Data;
using WorldCities.Data.Models;

namespace WorldCities.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class CitiesController : ControllerBase
    {
        private readonly ApplicationDbContext _context;

        public CitiesController(ApplicationDbContext context)
        {
            _context = context;
        }

        // GET: api/Cities
        [HttpGet]
        public async Task<ActionResult<IEnumerable<City>>> GetCities()
        {
            return await _context.Cities.ToListAsync();
        }

        // GET: api/Cities/5
        [HttpGet("{id}")]
        public async Task<ActionResult<City>> GetCity(int id)
        {
            var city = await _context.Cities.FindAsync(id);

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

            return city;
        }

        // PUT: api/Cities/5
        // To protect from overposting attacks, please enable the 
        // specific properties you want to bind to, for
        // more details see https://aka.ms/RazorPagesCRUD.
        [HttpPut("{id}")]
        public async Task<IActionResult> PutCity(int id, City city)
        {
            if (id != city.Id)
            {
                return BadRequest();
            }

            _context.Entry(city).State = EntityState.Modified;

            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
                if (!CityExists(id))
                {
                    return NotFound();
                }
                else
                {
                    throw;
                }
            }

            return NoContent();
        }

        // POST: api/Cities
        // To protect from overposting attacks, please enable the 
        // specific properties you want to bind to, for
        // more details see https://aka.ms/RazorPagesCRUD.
        [HttpPost]
        public async Task<ActionResult<City>> PostCity(City city)
        {
            _context.Cities.Add(city);
            await _context.SaveChangesAsync();

            return CreatedAtAction("GetCity", new { id = city.Id }, 
             city);
        }

        // DELETE: api/Cities/5
        [HttpDelete("{id}")]
        public async Task<ActionResult<City>> DeleteCity(int id)
        {
            var city = await _context.Cities.FindAsync(id);
            if (city == null)
            {
                return NotFound();
            }

            _context.Cities.Remove(city);
            await _context.SaveChangesAsync();

            return city;
        }

        private bool CityExists(int id)
        {
            return _context.Cities.Any(e => e.Id == id);
        }
    }
}

正如我们所看到的,代码生成器在遵循与我们的SeedController类类似的模式的同时做了很多有用的工作。以下是相关方法的分类,按外观顺序排列:

  • GetCities()返回包含数据库中所有城市的 JSON 数组。
  • GetCity(id)返回包含单个City的 JSON 对象。
  • PutCity(id, city)允许我们修改现有City
  • PostCity(city)允许我们添加新的City
  • DeleteCity(id)允许我们删除现有City

显然,我们的前端已经具备了所需的一切。在继续讨论 Angular 之前,让我们对Countries做同样的操作。

国家控制员

Solution Explorer中,右键点击/Controllers/文件夹,执行与我们添加CitiesController相同的任务集–除了名称之外,名称显然是CountriesController

为了简单起见,我们不会因为重复自动生成的代码而浪费额外的页面:毕竟,我们有一个专门的 GitHub 存储库来查找这些代码。但是,我们将获得与前面提到的处理国家/地区相同的方法集。

我们的实体框架之旅到此结束。现在,我们需要用我们最喜欢的前端框架将这些点连接起来,种植我们已经播种的东西。

总结

本章开始时,我们列举了一些没有合适的数据提供者就无法完成的事情。为了克服这些限制,我们决定为自己提供一个 DBMS 引擎和一个用于读取和/或写入数据的持久数据库。为了避免弄乱我们在前几章中所做的事情,我们创建了一个全新的 web 应用项目来处理这个问题,我们称之为WorldCities

然后,我们为我们的新项目选择了一个合适的数据源:一个世界城市和国家的列表,我们可以在一个方便的 MS Excel 文件中免费下载。

紧接着,我们转到了数据模型:实体框架核心似乎是获得我们想要的东西的一个明显选择,所以我们将其相关包添加到我们的项目中。我们简要列举了可用的数据建模方法,并由于其灵活性而采用了先使用代码的方法。完成后,我们创建了两个实体CityCountry,这两个实体都基于我们必须存储在数据库中的数据源值,以及一组数据注释和关系,利用了著名的实体框架核心的约定优于配置的方法。然后,我们建立了相应的ApplicationDbContext类。

创建数据模型后,我们评估了配置和部署 DBMS 引擎的各种选项:我们回顾了 DMBS 本地实例和基于云的解决方案,如 MS Azure,并解释了如何实现这两种解决方案。

最后但并非最不重要的一点是,我们创建了.NET 控制器类来处理数据:SeedController用于读取 Excel 文件并为数据库种子,CitiesController用于处理城市,CountriesController用于处理国家。

完成所有这些任务后,我们在调试模式下运行应用,以验证一切仍按预期工作。现在,我们准备搞乱我们应用的前端部分。在下一章中,我们将学习如何正确地从服务器获取这些数据,并以一种流行的方式将其提供给用户

好了,我们来了!

建议的主题

Web API、内存中 Web API、数据源、数据服务器、数据模型、数据提供程序、ADO.NET、ORM、实体框架核心、代码优先、数据库优先、模型优先、实体类、数据注释、DbContext、CRUD 操作、数据迁移、依赖项注入、ORM 映射、JSON、ApiController。

工具书类