作者 Julie Lerman | 2019 年 4 月 | 获取代码
我曾花了很多时间研究实体框架、EF Core 和 Docker。过去许多专栏文章都是证据。但到目前为止,我还没有结合使用它们。由于 Docker Tools for Visual Studio 2017 已推出一段时间,因此我以为它就是轻松突破口。但事实并非如此。这可能是因为,我既想知道这是怎么回事,同时还希望明确和理解我的选择。无论如何,我最终细查了博客文章、文章、GitHub 问题和 Microsoft 文档,才能实现最初的目标。通过将所有信息集中到一处,我希望本专栏的读者能够更容易地找到途径(并避免我遇到的一些小问题)。
我将以 Visual Studio 2017 为重点,而这意味着使用 Windows。因此,需要确保安装用于 Windows 的 Docker Desktop (dockr.ly/2tEQgR4),并将它设置为使用 Linux 容器(默认设置)。此外,还需要在计算机上启用 Hyper-V,但安装程序会在必要时提醒你。如果使用的是 Visual Studio Code(无论使用何种 OS),有很多扩展可便于直接在 IDE 中使用 Docker。
创建项目
我从简单的 ASP.NET Core API 入手。新建和我一样的项目的步骤如下:依次单击“新建项目”|“.NET Core”|“ASP.NET Core Web 应用”。在选择应用类型的页上,选择“API”。请务必选中“启用 Docker 支持”(见图 1)。将“OS”保留设置为“Linux”。Windows 容器更大、更复杂,但适用的托管选项仍相当有限。我好不容易才明白这一点。
图 1:配置新的 ASP.NET Core API 项目
由于启用了 Docker 支持,因此会在新项目中看到 Dockerfile。Dockerfile 向 Docker 引擎提供指令,以创建映像并根据最终映像运行容器。运行容器类似于实例化类中的对象。图 2 展示了模板创建的 Dockerfile。(请注意,我使用的是 Visual Studio 2017 版本 15.9.7,同时在计算机上安装了 .NET Core 2.2。随着 Docker 工具的发展,Dockerfile 也会随之演进。)
图 2:项目模板创建的默认 Dockerfile
FROM microsoft/dotnet:2.2-aspnetcore-runtime AS base
WORKDIR /app
EXPOSE 80
FROM microsoft/dotnet:2.2-sdk AS build
WORKDIR /src
COPY ["DataApi/DataApi.csproj", "DataApi/"]
RUN dotnet restore "DataApi/DataApi.csproj"
COPY . .
WORKDIR "/src/DataApi"
RUN dotnet build "DataApi.csproj" -c Release -o /app
FROM build AS publish
RUN dotnet publish "DataApi.csproj" -c Release -o /app
FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "DataApi.dll"]
第一条指令标识了用于创建后续映像的基础映像,且应用容器指定为:
microsoft/dotnet:2.2-aspnetcore-runtime
然后,在基础映像的基础之上创建生成映像。生成映像仅用于生成应用,因此它也需要 SDK。对生成映像执行大量命令,以将项目代码置于映像中,并在生成映像前还原所需的包。
创建的下一个映像将用于发布;它是在生成映像的基础之上生成。对于此映像,Docker 将运行 dotnet publish,以创建包含运行应用所需最少资产的文件夹。
最终映像不需要 SDK,它是通过基础映像创建而成。所有发布资产都将被复制到此映像中,且标识了入口点,即在此映像运行时什么应该会发生。
默认情况下,对于“调试”配置,Visual Studio 工具仅执行第一阶段,跳过发布映像和最终映像,但对于“发布”配置,使用的是整个 Dockerfile。
各种生成集称为“多阶段生成”,每一步都以不同的任务为重点。有趣的是,对映像执行的每个命令(如对生成映像执行的六个命令)都会导致 Docker 新建映像层。有关容器化 .NET 应用体系结构的 Microsoft 文档,请访问 bit.ly/2TeCbIu,其中逐行细致地解释了 Dockerfile,并介绍了它是如何通过多阶段生成提高效率的。
我将暂时保留 Dockerfile 的默认设置。
在 Docker 中调试默认控制器
直奔 Docker 调试前,我将先在 ASP.NET Core 自承载配置文件(它使用 Kestrel,这是用于 ASP.NET Core 的跨平台 Web 服务器)中调试,从而验证应用。请务必将“开始调试”按钮(工具栏上的绿色箭头)设置为,使用与项目名称匹配的配置文件(在此示例中为“DataAPI”)运行。
然后运行应用。此时,浏览器应该打开,并指向 URL http://localhost:5000/api/values,同时显示默认控制器方法结果(“value1”、“value2”)。现在你知道应用可以正常运行,是时候在 Docker 中调试它了。停止应用,并将调试配置文件更改为“Docker”。如果用于 Windows 的 Docker 正在运行(且使用本文开头所述的相应设置),Docker 便会运行 Dockerfile。如果你以前从未拉取过引用的映像,Docker 引擎会先从 Docker 中心拉取这些映像。拉取映像可能需要几分钟才能完成。可以在“生成输出”窗口中查看拉取进度。然后,Docker 会按照 Dockerfile 中的步骤生成任意映像,但它不会重新生成自上次运行以来没有更改的任何映像。最后一步是由 Visual Studio Tools for Docker 执行,它将先调用 docker build,再调用 docker run,以启动容器。这样一来,新的浏览器窗口(或选项卡)会打开,并显示与之前相同的输出,但 URL 不同,因为它来自公开相应端口的 Docker 映像内部。在此示例中,URL 是 http://172.26.137.194/api/values。备用设置会导致浏览器使用 http://localhost:hostPort 启动。
如果 Visual Studio 无法运行 Docker
我遇到了两个问题,最初导致 Docker 无法生成映像。第一次尝试在 Visual Studio 定目标到的 Docker 中调试时,我看到了以下消息:“尝试运行 Docker 容器时出错了。” 此错误引用了 container.targets 第 256 行。直到后来意识到我本可以在“生成输出”中查看错误详细信息时,才发现此为信息丰富的有用消息。在尝试了各种解决方案(包括在 Internet 上大量阅读信息,但仍未检查“生成输出”窗口)后,我最后尝试从 Docker CLI 拉取映像,但它提示我登录 Docker,尽管我已登录 Docker Desktop 应用。这样做后,我便能在 Visual Studio 2017 中调试。随后,通过 Docker CLI 注销并不影响,我仍可调试。我不确定这两个操作之间的关系。不过,在我完全卸载和重新安装用于 Windows 的 Docker Desktop 后,系统再次强制必须先通过 Docker CLI 登录,然后才能运行应用。根据 GitHub 上的问题 (bit.ly/2Vxhsx4),这似乎是因为我使用电子邮件地址(而不是登录名)登录用于 Windows 的 Docker。
在禁用 Hyper-V 后,我也看到过一次相同的错误。重新启用 Hyper-V 并重启计算机后,此问题解决了。(出于好奇,我需要运行 VirtualBox 虚拟机来执行完全不相关的任务,而 VirtualBox 要求禁用 Hyper-V。)
Docker 引擎管理什么?
由于应用是第一次运行,因此 Docker 引擎从 Docker 中心 (hub.docker.com) 拉取两个已指出的映像,并一直跟踪它们。但 Dockerfile 还创建了它已缓存的其他映像。在命令行处运行 docker 映像揭示了用于 Windows 的 Docker 本身使用的 docker4w 映像、从 Docker 中心拉取的 aspnetcore-runtime 映像,以及通过生成 Dockerfile 创建的 dataapi:dev 映像(即应用从中运行的映像)。如果运行 docker images -a 来显示隐藏的映像,便会看到另外两个映像(没有标记),它们是由 Dockerfile 创建的生成和发布中间映像,如图 3 所示。你不会看到任何关于 SDK 映像的内容,根据来自 Microsoft 的 Glenn Condron 的解释:“这是由于 Docker 多阶段生成工作方式中的怪癖所致。”
图 3:运行 API 后揭示的公开和隐藏 Docker 映像
若要详细了解映像,可以运行下面的命令:
docker image inspect [imageid]
容器又如何呢?命令 docker ps 揭示了 Docker Tools for Visual Studio 2017 通过对开发映像调用 docker run(带参数)创建的容器。我已在图 4 中堆叠结果,这样你就能看到所有列。没有隐藏的容器。
图 4:通过使用 Visual Studio 2017 调试配置运行应用而创建的 Docker 容器
创建数据 API
现在将使用 EF Core 作为数据暂留机制,将这转换为数据 API。模型过于简单化是为了将重点放在容器化及其对 EF Core 数据源的影响上。
首先,添加名为“Magazine.cs”的类:
publicclassMagazine{publicintMagazineId {get;set; }publicstringName {get;set; }}
接下来,需要安装三个不同的 NuGet 包。由于我将展示使用独立式 SQLite 数据库和 SQL Server 数据库之间的区别,因此请同时将 Microsoft.EntityFrameworkCore.Sqlite 和 Microsoft.EntityFrameworkCore.SqlServer 包添加到项目中。因为还将运行 EF Core 迁移,所以要安装的第三个包是 Microsoft.EntityFrameworkCore.Design。
现在,我将让工具为 API 创建控制器和 DbContext。如果你是初次接触,请按照以下步骤操作:
右键单击“解决方案资源管理器”中的“控制器”文件夹。
依次选择“添加”、“控制器”,以及包含操作且使用实体框架的 API 控制器。
选择 Magazine 类作为 Model 类。
单击 Data 上下文类旁边的加号,将名称的突出显示部分更改为“Mag”,这样它就变成“[YourApp].Models.MagContext”,再单击“添加”。
保留默认控制器名称“MagazinesController”。
单击“Add”(添加)。
完成后,便会新建含 MagContext 类的“数据”文件夹,“控制器”文件夹会包含新的 MagazineController.cs 文件。
现在,我将使用我在 2018 年 8 月专栏文章 (msdn.com/magazine/mt829703) 中提到的基于 DbContext 的种子设定,让 EF Core 在数据库中播种三个杂志。将以下方法添加到 MagContext.cs 中:
protectedoverridevoidOnModelCreating(ModelBuilder modelBuilder){ modelBuilder.Entity().HasData(newMagazine { MagazineId = 1, Name ="MSDN Magazine"},newMagazine { MagazineId = 2, Name ="Docker Magazine"},newMagazine { MagazineId = 3, Name ="EFCore Magazine"} ); }
创建数据库
若要创建数据库,我需要指定提供程序和连接字符串,然后创建并运行迁移。我想从熟悉的方法入手,所以先定目标到 SQL Server LocalDB,并在 appsettings.json 文件中指定连接字符串。
打开 appsettings.json 后,你会发现它已包含连接字符串,这是控制器工具在我让它定义 MagContext 文件时创建的。尽管同时安装了 SQL Server 和 SQLite 提供程序,但似乎已默认为使用 SQL Server 提供程序。随后的测试证明了这一点。我更想使用自己的连接字符串名称和数据库名称,因此我将 MagContext 连接字符串替换为 MagsConnectionMssql,并添加了我要使用的数据库名称 DP0419Mags:
"ConnectionStrings": {"MagsConnectionMssql":"Server=(localdb)\\mssqllocaldb;Database=DP0419Mags;Trusted_Connection=True;"}
在应用的 startup.cs 文件中(其中包括 ConfigureServices 方法),此工具还插入了用于配置 DbContext 的代码。将它的连接字符串名称从 MagContext 更改为匹配新名称:
services.AddDbContext(options => options.UseSqlServer(Configuration.GetConnectionString("MagsConnectionMssql")));
现在,我可以使用 EF Core 迁移来创建首个迁移,因为我使用的是 Visual Studio,我可以在包管理器控制台中使用 PowerShell 命令来完成此操作:
add-migration initMssql
迁移数据库
上面的命令创建了迁移文件,但我并不打算使用迁移命令来创建数据库。也就是说,在部署应用时,我不希望必须要执行迁移命令才能创建或更新数据库。我将改用 EF Core Database.Migrate 方法。在应用中使用此逻辑方法是一个重要决定。需要在应用启动时就运行它。很多人将此解释为当 startup.cs 文件运行时,但 ASP.NET Core 团队建议将应用启动代码置于 program.cs 文件中,这才是 ASP.NET Core 应用的真正起点。但与任何决定一样,很可能有一些因素会影响你遵循这项指导。
程序的默认 Main 方法调用 ASP.NET Core 方法 CreateWebHostBuilder,它代表你执行很多任务,然后调用另外两个方法,即 Build 和 Run:
publicstaticvoidMain(string[] args){ CreateWebHostBuilder(args).Build().Run();}
我需要在调用 Build 后但在调用 Run 前迁移数据库。为此,我创建了读取 startup.cs 中定义的服务提供程序信息的扩展方法,它将发现 DbContext 配置。然后,此方法根据上下文调用 Database.Migrate。我将 GitHub 问题 (bit.ly/2T19cbY) 中的代码(以及 EF Core 团队成员 Brice Lambson 的指导)改编为,创建 IWebHost 的扩展方法,如图 5 所示。此方法需要使用泛型 DbContext 类型。
图 5:IWebHost 的扩展方法
publicstaticIWebHost MigrateDatabase(thisIWebHost webHost)whereT : DbContext{using(varscope = webHost.Services.CreateScope()) {varservices = scope.ServiceProvider;try{vardb = services.GetRequiredService(); db.Database.Migrate(); }catch(Exception ex) {varlogger = services.GetRequiredService>(); logger.LogError(ex,"An error occurred while migrating the database."); } }returnwebHost;}
然后,我将 Main 方法修改为,在 Bulid 和 Run 之间为 MagContext 调用 MigrateDatabase:
CreateWebHostBuilder(args).Build().MigrateDatabase<MagContext>().Run();
在你添加所有此类新代码时,Visual Studio 应该会提示你为 Microsoft.EntityFrameworkCore、Microsoft.Extensions.DependencyInjection 和 MagContext 类的命名空间添加 using 语句。
现在,数据库会根据需要在运行时迁移(或甚至创建)。
调试前的最后一步是,指示 ASP.NET Core 在启动时指向 Magazines 控制器,而不是 values 控制器。为此,可以在 launchsettings.json 文件中,将 launchUrl 实例从 api/values 更改为 api/Magazines。
依次在 Kestrel 和 Docker 中运行数据 API
就像我对 values 控制器所做一样,我将先通过项目配置文件(例如,DataAPI)在自承载服务器上对此进行测试,而不是通过 Docker 配置文件。因为数据库尚不存在,所以 Migrate 会新建数据库。也就是说,会有短暂延迟,因为 SQL Server(甚至 LocalDB)有很多工作要做。但确实会创建和播种数据库,然后默认控制器方法读取并在浏览器中的 localhost:5000/api/Magazines 处显示三个杂志。
现在用 Docker 试试看。将调试配置文件更改为“Docker”,并再次运行它。哦不! 打开的浏览器显示 SQLException,其中详细说明了 TryGetConnection 失败。
这行代码起什么作用呢?应用在正在运行的 Docker 容器中查找 SQL Server(在连接字符串中定义为“(localdb)\\mssqllocaldb”)。但 LocalDB 安装在我的计算机上,而不是容器中。尽管这是在开发环境中为 SQL Server 数据库做准备时的一种常见选择,但在定目标到 Docker 容器时,就不那么容易行得通了。
也就是说,我有更多工作要做,你也可能会遇到更多问题。我就是这样。
简单变通方法
有一些很棒的方法,比如在另一个 Docker 容器中使用 SQL Server for Linux,或定目标到 SQL Azure 数据库。在接下来的几篇文章中,我将深入研究这些解决方案,但我要先介绍一个快速解决方案,它可确保数据库服务器确实存在于容器中,且 API 将成功运行。可使用独立式数据库 SQLite 轻松实现此目的。
你应该已安装 Microsoft.EntityFramework.SQLite 包。此 NuGet 包的依赖项会强制 SQLite 运行时组件在生成应用的映像中安装。
向 appsettings.json 文件添加新连接字符串 MagsConnectionSqlite。我已将文件名指定为“DP0419Mags.db”:
"ConnectionStrings": {"MagsConnectionMssql":"Server=(localdb)\\mssqllocaldb;Database=DP0419Mags;Trusted_Connection=True;","MagsConnectionSqlite":"Filename=DP0419Mags.db;"}
在 Startup 中,使用新连接字符串名称将 DbContext 提供程序更改为 SQLite:
services.AddDbContext(options => options.UseSqlite(Configuration.GetConnectionString("MagsConnectionSqlite")));
创建的迁移文件为 SQL Server 提供程序专用,因此需要替换它。删除整个“迁移”文件夹,然后在包管理器控制台中运行 Add-Migration initSqlite,以重新创建此文件夹以及迁移文件和快照文件。
若要查看创建的文件,可以对内置服务器运行此命令;也可以在 Docker 中直接开始调试。SQLite 数据库在 Migrate 命令调用时快速新建,然后浏览器再次显示三个杂志。请注意,URL 的 IP 地址与前面在 Docker 中运行 values 控制器时看到的 IP 地址一致。在此示例中,IP 地址为 http://172.26.137.194/api/Magazines。所以,API 和 SQLite 现在同时在 Docker 容器中运行。
即将推出更适合生产的解决方案
虽然使用 SQLite 数据库确实简化了让 EF Core 在运行应用的相同容器中创建数据库的任务,但这很可能不是你希望将 API 部署到生产环境中的方式。容器的优点之一是,可以使用和协调多个容器来表示关注分离。
在这个小型解决方案中,或许 SQLite 就可以了。但对于实际解决方案,应利用其他选项。着眼于 SQL Server,选项之一就是定目标到 Azure SQL 数据库。使用此选项,无论在何处运行应用(在开发计算机上、在 IIS 中、在 Docker 容器中,还是在云内的 Docker 容器中),都可以确保始终指向一致的数据库服务器或数据库,具体视需求而定。另一种方法是,利用容器化数据库服务器(如 SQL Server for Linux),正如我在前面的一篇专栏文章 (msdn.com/magazine/mt784660) 中所述。微服务引入了另一层可能的解决方案,假定指导原则是每个微服务有一个数据库。也可以在容器中轻松管理它们。Microsoft 推出过一本关于为容器化微服务构建 .NET 应用的超棒免费书籍 (bit.ly/2NsfYBt)。
在接下来的几篇专栏文章中,我将介绍其中一些解决方案,展示如何定目标到 SQL Azure 或容器化 SQL Server;如何使用 Docker 环境变量管理连接字符串和保护凭据;以及如何让 EF Core 能够在设计时使用迁移命令发现连接字符串,并在运行时从 Docker 容器中发现连接字符串。即使我之前有使用 Docker 和 EF Core 的经验,但在具体了解这些解决方案的学习过程中我也遇到了许多波折,很期待与你一同分享。
Julie Lerman 住在佛蒙特州的丘陵地区,担任 Microsoft 区域主管、Microsoft MVP、软件团队导师和顾问。可以在全球的用户组和会议中看到她对数据访问和其他主题的介绍。她的博客地址是 thedatafarm.com/blog。她是“Entity Framework 编程”及其 Code First 和 DbContext 版本(全都出版自 O’Reilly Media)的作者。请通过 Twitter 关注她 (@julielerman),并观看她在 bit.ly/PS-Julie 上的 Pluralsight 课程。
衷心感谢以下 Microsoft 技术专家对本文的审阅:Glenn Condron、Steven Green、Mike Morton
网友评论