引言
在第1章 .NET Aspire 简介中,我们为将要构建的电子商务应用程序奠定了基础。有关应用程序架构图的详细信息,请参阅第1章。在本章中,你将了解 .NET Aspire 集成的概念,以及它如何用于创建我们用 .NET 语言编写的第一个 API(仓库)。此 API 将作为管理库存的主要组件。另一个关键组件,数据 API 构建器 (DAB),DAB 是一个工具,可直接根据数据库(表)自动生成 REST 和 GraphQL 端点,从而显著减少 CRUD 操作中的样板代码。
结构
在本章中,我们将讨论以下主题:
- 设置开发环境
- .NET Aspire 集成的好处
- 理解 .NET Aspire 集成
- 数据 API 构建器
- 创建仓库后端 API
- 理解代码结构
- 将数据 API 构建器添加到 AppHost
- .NET Aspire 社区工具包
- 从 Visual Studio 运行 Eshop Aspire
- 在 Visual Studio 中使用 .http 文件验证 API
目标
在本章结束时,你将全面了解 .NET Aspire 集成及其在构建分布式应用程序中的实际用途。你将熟悉 DAB,它在管理数据库交互方面发挥着重要作用。你将能够使用 .NET 语言创建一个功能性的仓库 API,并能够使用 HTTP 文件对其进行测试。
在我们开始之前,让我们先看一些先决条件。
设置开发环境
请确保通过安装或设置以下资源来设置和配置你的开发环境:
- 具有活动订阅的 Azure 帐户 https://azure.microsoft.com/free/?ref=microsoft.com&utm_source=microsoft.com&utm_medium=docs&utm_campaign=visualstudio
- Dotnet 9.0 或更高版本 https://dotnet.microsoft.com/en-us/download
- .NET Aspire SDK https://learn.microsoft.com/en-us/dotnet/aspire/fundamentals/dotnet-aspire-sdk
- PowerShell 7.0 或更高版本(仅限 Windows 用户!)https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell-on-windows?view=powershell-7.4#installing-the-msi-package
- 安装 DAB https://learn.microsoft.com/en-us/azure/data-api-builder/
- Docker Desktop https://docs.docker.com/desktop/install/windows-install/
- Visual Studio Code https://code.visualstudio.com/ (或) Visual Studio 2022 - https://visualstudio.microsoft.com/vs/
- VS Code Docker 扩展 https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker
- Azure CLI https://docs.microsoft.com/cli/azure/install-azure-cli
- Git CLI https://git-scm.com/
注意: 在撰写本书时,我们通篇使用了 .NET Aspire 9.2 版本。但是,我们在第8章末尾介绍了 .NET Aspire 9.3 版本中发布的一些重要功能,即 .NET Aspire 和 AI。
.NET Aspire 集成的好处
当我们处理分布式或云原生应用程序时,我们不仅要考虑代码本身。如果我们考虑一个简单的分布式应用程序,我们可能会想到一个从后端请求数据的前端。即使在这样一个简单的场景中,前端也很可能会有一个缓存来存储数据,以防止过于频繁地调用后端。另一个例子可能是有一个队列来处理不同后端服务之间的调用。
这种常见的模式促使 .NET Aspire 团队思考集成。这些是在 .NET Aspire 应用程序中托管和使用缓存、队列等常见资源的方法。.NET Aspire 的 NuGet 包旨在协同工作,帮助开发人员专注于他们的应用程序逻辑,而不是花时间处理基础设施的复杂性。
理解 .NET Aspire 集成
首先,有两种类型的集成:托管资源和客户端资源。托管资源实际上会启动某个资源的本地实例,比如一个 SQL 服务器或一个 Redis 缓存。客户端资源将有助于在你的应用程序内部使用某个资源,将客户端库连接到依赖注入。(注意:并非所有集成都属于这两个类别)。例如,有一个针对 OpenAI 的集成,但它不会启动 OpenAI 的本地实例。它将让你配置一个到现有 OpenAI 资源的连接字符串,并配置要在你的代码中使用的客户端。
为了更好地理解这两种集成类型之间的差异,我们将考虑 Redis。这是一个我们既有托管资源又有客户端资源的场景。
在上一章中,我们已经看到了 aspire-starter 模板。我们现在可以研究相同的模板,用 --use-redis-cache 标志调用它。首先,在解决方案文件夹内打开命令行,然后输入:
Dotnet new aspire-starter --use-redis-cache
此命令将创建一个与我们已经分析过的项目相同的模板。主要区别可以在 AppHost 和 web 项目中看到。
在 AppHost.csproj 文件中,我们可以看到对包 Aspire.Hosting.Redis 的引用:
<PackageReference Include="Aspire.Hosting.Redis" Version="9.0.0" />
这是托管集成,正如包名中的 Hosting 所建议的那样。在 Web.csproj 文件中,我们可以找到对 Aspire.StackExchange.Redis.OutputCaching 的引用:
<PackageReference Include="Aspire.StackExchange.Redis.OutputCaching" Version="9.0.0" />
这是 .NET Aspire 针对 Redis 的客户端集成。从此模板中得到的第一个启示是,即使我们正在处理一个同时处理托管和客户端的集成,我们也需要根据集成类型分别在应用主机和应用中添加两个不同的包。
如果我们打开 AppHost 项目中的 Program.cs,我们可以看到托管集成的代码:
var builder = DistributedApplication.CreateBuilder(args);
var cache = builder.AddRedis("cache");
var apiService =
builder.AddProject<Projects.Demo1_ApiService>("apiservice");
builder.AddProject<Projects.Demo1_Web>("webfrontend")
.WithExternalHttpEndpoints()
.WithReference(cache)
.WaitFor(cache)
.WithReference(apiService)
.WaitFor(apiService);
builder.Build().Run();
我们感兴趣的行是:
var cache = builder.AddRedis("cache");
在这里,我们添加了一个称之为 cache 的 Redis 缓存。资源名称与应用程序编程接口(API)服务和前端的资源名称具有相同的功能:我们可以使用它通过服务发现来引用资源。
我们现在可以打开 Web 项目中的 Program.cs,看看我们如何利用 Redis 客户端集成:
using Demo1.Web;
using Demo1.Web.Components;
var builder = WebApplication.CreateBuilder(args);
// 添加服务默认值和 Aspire 客户端集成。
builder.AddServiceDefaults();
builder.AddRedisOutputCache("cache");
// 向容器添加服务。
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
builder.Services.AddHttpClient<WeatherApiClient>(client =>
{
// 此 URL 使用 "https+http://" 来表示 HTTPS 优先于 HTTP。
// 在 https://aka.ms/dotnet/sdschemes 了解有关服务发现方案解析的更多信息。
client.BaseAddress = new("https+http://apiservice");
});
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error", createScopeForErrors: true);
// 默认的 HSTS 值为 30 天。你可能希望为生产场景更改此设置,请参阅 https://aka.ms/aspnetcore-hsts。
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseAntiforgery();
app.UseOutputCache();
app.MapStaticAssets();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.MapDefaultEndpoints();
app.Run();
在第八行,我们找到:
builder.AddRedisOutputCache("cache");
这就是引用在 AppHost 中定义的名为 cache 的 Redis 缓存的方式。
运行项目可以帮助我们更好地理解底层发生的事情。如果我们在没有容器引擎运行的机器上运行项目,我们会看到 cache 资源不健康。启动容器引擎将导致状态变为 正在运行。这就是 .NET Aspire 仪表板应该看起来的样子,如图2.1所示:
注意: 在 类型 列中,我们现在有一个 容器 资源,这是我们的 Redis 缓存。我们还可以在终端中通过启动命令
docker ps来找到正在运行的容器。
与项目资源的环境变量注入发生的情况类似,我们可以在以下图2.2中看到,在 webfrontend 项目资源内部有一个用于新创建的 Redis 缓存的环境变量:
注意: 请注意 .NET Aspire 如何自动将缓存(Redis)资源的连接字符串注入到
webfrontend项目中。
有许多不同的 .NET Aspire 集成,从数据库(例如,SQL 和 PostgresDB)到缓存(例如,Redis)再到云资源(例如,Azure Service Bus 和 OpenAI)。在一本书中描述所有这些将是一项不可能完成的任务,但我们将在我们的架构中使用其中的一些来展示使用它们的好处。我们建议你深入研究文档以发现所有可用的集成。
现在我们对 .NET 集成有了大致的了解,我们可以开始使用 DAB 从数据端构建我们的应用程序。之后,我们将能够将 DAB 作为容器添加到我们的 AppHost,然后利用集成来简化我们的代码。
数据 API 构建器
数据始终是任何现代应用程序开发的核心。在这段时间里,我们看到了许多不同的方法和工具来帮助开发人员将业务层连接到数据库。通常,开发人员会编写相同的代码来执行 创建、读取、更新、删除 (CRUD) 操作。DAB 的创建是为了帮助开发人员使用一个灵活而强大的框架来快速生成 API,从而帮助弥合应用程序层和数据库之间的差距。
DAB 是一个开源(在 麻省理工学院 (MIT) 许可下)的跨平台框架,你可以在其中启动现代 API 端点,以对任何数据库执行 CRUD 操作。DAB 支持多种数据库,包括 SQL Server、Azure SQL、用于 NoSQL 的 Azure Cosmos DB、PostgreSQL 等。DAB 支持 REST 和 GraphQL 端点,并且可以在本地或容器中执行。有关 DAB 的更多详细信息,请参阅微软的官方文档 https://learn.microsoft.com/en-us/azure/data-api-builder/overview。由于它是开源的,任何感兴趣的人都可以通过查看其开放问题 https://github.com/azure/data-api-builder/labels/known-issue 来做出贡献。
要安装 DAB 的命令行:
dotnet tool install microsoft.dataapibuilder -global
在 DAB 中,配置文件是其操作的支柱。配置文件是一个强大、灵活的工具,可帮助开发人员构建健壮的 API。它本质上是声明性的,这不仅减少了开发时间,而且还确保了 API 管理的一致性和可伸缩性。
init 命令用于生成一个名为 dab-config.json 的新配置文件。它包含有关数据库类型(例如,MSSQL、SQLServer、Cosmos DB)、连接字符串、选择哪个端点(GraphQL/REST 或两者)以及身份验证详细信息:
dab init
要验证配置文件,请运行以下命令,该命令会检查语法错误并确保所有必填字段都已填充:
dab validate dab-config.json
创建仓库后端 API
一旦你有了开发环境并正在运行,就该为我们创建第一个 API 后端服务(仓库 API)了。
你可以使用 Visual Studio 或 命令行界面 (CLI) 来创建和初始化带有仓库 API 的 .NET Aspire 项目。
使用命令行界面
让我们首先打开一个新的终端会话并运行以下命令来创建一个空文件夹并创建我们的入门应用:
mkdir Eshop
cd Eshop
dotnet new Aspire.AppHost -n Eshop.AppHost
我们可以附加标志来添加集成,但对于本章来说,这是不需要的。
使用 Visual Studio 2022
在 Visual Studio 的顶部,导航到 文件 | 新建 | 项目。在对话框窗口中(如图2.3所示),搜索 Aspire 并选择 .NET Aspire 空应用 模板:
单击 下一步 以添加项目名称和其他信息,如图2.4所示:
单击 下一步 以选择 框架 为 .NET 9.0,如图2.5所示:
在我们首选的编辑器中打开创建的解决方案后,我们将在解决方案资源管理器中看到两个项目:
- Eshop.AppHost
- Eshop.ServiceDefaults
让我们添加 ASP.NET Core Web API 项目,右键单击解决方案以选择 添加新项目 并在模板页面中搜索 web API,如图2.6所示:
单击 下一步 以输入项目名称为 WarehouseAPI 和位置。单击 下一步 以输入其他详细信息,如图2.7所示,以将其创建为没有控制器的最小 API:
注意: 我们正在创建一个最小 API,因此我们取消选中 使用控制器,然后我们选中 启用 OpenAPI 支持,以便 OpenAPI 将帮助我们记录和测试我们的 API。
我们现在将在 Visual Studio 解决方案资源管理器 窗格中看到三个项目,如图2.8所示:
- Eshop.AppHost
- Eshop.ServiceDefaults
- WarehouseAPI
理解代码结构
让我们看一下这个项目中的一些重要文件,以进一步讨论。
dab-config.json
在根文件夹中创建一个 dab 文件夹,并从命令行运行 dab init 以生成 dab-config.json 文件。如下所示更新文件,以便为 WarehouseDB 内的 WarehouseItems 和 Orders 表创建 API 端点。在本章中,我们指定了匿名权限,这将在本书的未来章节中进行更新:
{
"data-source": {
"database-type": "mssql",
"connection-string": "@env('ConnectionStrings_WarehouseDB')"
},
"runtime": {
"rest": {
"path": "/api"
},
"graphql": {
"path": "/graphql"
},
"host": {
"cors": {
"origins": [],
"allow-credentials": false
},
"mode": "development"
}
},
"entities": {
"WarehouseItems": {
"source": "WarehouseItems",
"permissions": [
{
"role": "anonymous",
"actions": [
"create",
"read",
"update",
"delete"
]
}
],
"rest": {
"path": "WarehouseItems"
}
},
"Orders": {
"source": "Orders",
"permissions": [
{
"role": "anonymous",
"actions": [
"create",
"read",
"update",
"delete"
]
}
],
"rest": {
"path": "Orders"
}
}
}
}
上述 JSON 文件中的一些关键部分是:
- data-source: 此部分定义数据库连接类型和连接字符串。在这种情况下,它指定数据库类型为
mssql,并且连接字符串具有一个环境占位符,aspire 将在运行时替换它。 - runtime: 此部分概述了 API 的运行时配置。它包括 REST (
/api) 和 GrapQL (/graphql) 端点的路径,以及诸如 跨域资源共享 (CORS) 配置之类的主机设置,并且模式设置为development。 - entities: 此部分定义 API 将与之交互的实体。例如,
WarehouseItems和Orders被指定为源,具有匿名角色执行 CRUD 等操作的权限。每个实体还定义了一个用于访问它们的 REST 路径。
Warehouseclient.cs
Warehouseclient.cs 旨在与 Web API 交互,以获取仓库项目并筛选出与待处理订单关联的项目。它使用 HttpClient 发出 HTTP 请求并处理响应以返回相关的仓库项目。记录定义了 API 响应中使用的 数据传输对象 (DTO)。
- DABResponse: 表示来自 DAB 的响应的通用记录。它包含一个类型为 T 的项目列表。
- WarehouseItem: 表示仓库项目的记录,具有诸如
ItemID、ItemName、Stock和LastUpdated之类的属性。 - Order: 表示订单的记录,具有诸如
OrderID、CustomerName、ItemID、Quantity、Status、OrderDate和LastUpdated之类的属性。
using System.Linq;
namespace WarehouseAPI;
public class WarehouseClient(HttpClient httpClient)
{
public async Task<WarehouseItem[]> GetWarehouseStatus(CancellationToken cancellationToken = default)
{
List<WarehouseItem> items = new();
var response = await httpClient.GetFromJsonAsync<DABResponse<WarehouseItem>>("api/WarehouseItems", cancellationToken: cancellationToken);
var pendingOrders = await httpClient.GetFromJsonAsync<DABResponse<Order>>("api/Orders?$filter=Status eq 'Pending'", cancellationToken: cancellationToken);
if (response != null && pendingOrders != null)
{
items.AddRange(response.Value.Where(item => !pendingOrders.Value.Select(o => o.ItemID).Contains(item.ItemID)));
}
return items.ToArray();
}
}
public record DABResponse<T>(List<T> Value);
public record WarehouseItem(int ItemID, string ItemName, int Stock, DateTime LastUpdated);
public record Order(int OrderID, string CustomerName, int ItemID, int Quantity, string Status, DateTime OrderDate, DateTime LastUpdated);
DABResponse<T> 记录对来自 DAB 的分页响应的典型结构进行建模,其中包含一个 value 属性,该属性是所请求实体的列表。
WarehouseClient 类是通过使用 主构造函数 编写的,它允许你直接在类声明中定义构造函数,从而使代码更简洁。这意味着当你创建此类的实例时,你必须向其传递一个 HttpClient 对象。HttpClient 通常是注入的(依赖注入),这是现代 .NET 应用程序中管理依赖项和改进测试的常见做法。
此类包含一个名为 GetWarehouseStatus 的单一方法。此方法获取仓库项目的当前状态。它通过调用由 DAB 创建的 REST API 端点来发出两个异步 HTTP Get 请求,以使用 HttpClient 检索仓库项目和订单。
launchsettings.json
launchsettings.json 文件用于配置应用程序在开发过程中的启动方式。此文件通常位于项目的 properties 文件夹中。它包含定义环境变量、命令行参数和用于运行应用程序的配置文件的设置:
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "api/warehousestatus",
"applicationUrl": "http://localhost:5108",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "api/warehousestatus",
"applicationUrl": "https://localhost:7262;http://localhost:5108",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
注意: 端口号 7262 和 5108 是自动生成的,当你创建 API 项目时,你可能会看到不同的端口号。
appsettings.json
appsettings.json 文件是一个配置文件,用于以结构化格式存储设置和配置信息。此文件用于配置应用程序的各个方面,例如:日志记录、连接字符串和自定义设置:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"DabConfig": {
"BaseUrlHttp": "http://localhost:5001/"
}
}
注意:
BaseUrlHttp指的是 DAB 在其自己的容器内侦听的端口。.NET Aspire 将管理此服务的外部公开,通常在不同的端口上,并提供服务发现(例如,https+http://dab)以供其他服务与之通信。
程序配置
Program.cs 文件是 WarehouseAPI 项目的入口点,它为后端 API 设置必要的基础设施,包括依赖注入、中间件、路由和服务配置,并定义 HTTP 请求管道。它通常包含 main 方法,这是起点。但是,从 .NET 6 或更高版本开始,Program.cs 文件通常使用顶级语句来简化代码。
由于我们创建了一个最小 API 的 WarehouseAPI 项目(没有控制器),因此直接在 Program.cs 内部创建一个新的 GET 端点 /api/warehousestatus,并配置异常处理程序和映射默认端点。
Program.cs 文件的全部内容如下所示:
using System.Text.Json;
using WarehouseAPI;
var builder = WebApplication.CreateBuilder(args);
// 添加服务默认值和 Aspire 组件。
builder.AddServiceDefaults();
// 向容器添加服务。
builder.Services.AddProblemDetails();
builder.Services.AddHttpClient<WarehouseClient>(client =>
{
// 此 URL 使用 "https+http://" 来表示 HTTPS 优先于 HTTP。
// 在 https://aka.ms/dotnet/sdschemes 了解有关服务发现方案解析的更多信息。
client.BaseAddress = new("https+http://dab");
});
var app = builder.Build();
// 配置 HTTP 请求管道。
app.UseExceptionHandler();
app.MapGet("/api/warehousestatus", async (WarehouseClient warehouseClient, HttpResponse response) =>
{
var items = await warehouseClient.GetWarehouseStatus();
await JsonSerializer.SerializeAsync(response.Body, items);
});
app.MapDefaultEndpoints();
app.Run();
WebApplication.CreateBuilder(args) 初始化 WebApplicationBuilder 类的新实例,该实例设置应用程序配置、日志记录和依赖注入。
将数据 API 构建器添加到 AppHost
现在我们已经了解了 DAB 的工作原理,我们需要向我们的 AppHost 项目添加一个 SQL 数据库和一个 DAB 实例。
首先添加所需的引用:
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="Aspire.AppHost.Sdk" Version="9.0.0-rc.1.24511.1" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsAspireHost>true</IsAspireHost>
<UserSecretsId>42534081-c920-4a7c-9716-a23d8cb0c4a1</UserSecretsId>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Aspire.Hosting.AppHost" />
<PackageReference Include="Aspire.Hosting.SqlServer" />
<PackageReference Include="CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\WarehouseAPI\WarehouseAPI.csproj" />
</ItemGroup>
</Project>
在 Eshop.AppHost/Program.cs 中,我们可以添加以下代码:
var builder = DistributedApplication.CreateBuilder(args);
// 添加一个 SQL Server 容器
var sqlPassword = builder.AddParameter("sql-password");
var sqlServer = builder.AddSqlServer("sql", sqlPassword);
// 取消注释此行以在运行之间持久化数据库
sqlServer.WithLifetime(ContainerLifetime.Persistent);
var sqlDatabase = sqlServer.AddDatabase("WarehouseDB");
// 使用模式和数据填充数据库
sqlServer
.WithBindMount("./sql-server", target: "/usr/config")
.WithBindMount("../../../db-scripts", target: "/docker-entrypoint-initdb.d")
.WithEntrypoint("/usr/config/entrypoint.sh");
var dab = builder.AddDataAPIBuilder("dab", "../../../dab/dab-config.json")
.WithReference(sqlDatabase)
.WaitFor(sqlServer);
builder.AddProject<Projects.WarehouseAPI>("warehouseapi")
.WithReference(dab)
.WaitFor(dab);
builder.Build().Run();
在这段代码中引入了一些新东西。在 .AddSqlServer("sql", sqlPassword); 行中,我们添加了一个将保存 SQL 数据库密码的参数。此参数在 AppHost 项目的 appsettings.json 文件中定义:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Aspire.Hosting.Dcp": "Warning"
}
},
"Parameters": {
"sql-password": "P_ssw0rd!"
}
}
然后,我们可以找到添加一个我们称之为 SQL 的 SQL Server 的行。这是一个托管资源,它将创建一个本地 SQL Server 容器的新实例。
以下行处理 SQL 容器的生命周期。由于每次应用程序启动时我们都会为数据库播种,我们可以利用 .NET Aspire 容器生命周期管理来在不同执行之间持久化容器。
sqlServer.WithLifetime(ContainerLifetime.Persistent);
一旦我们有了一个持久的 SQL Server,我们就可以向该服务器添加一个数据库,我们称之为 WarehouseDB。
现在我们需要做的就是为数据库播种,我们将使用 Jerry Nixon 在一个公共 GitHub 存储库中提供的一些脚本:https://github.com/JerryNixon/aspire-sqlserver/tree/main/202411-DotnetConf。
要使用这些脚本,请在 Eshop.AppHost 文件夹内创建一个 sql-server 文件夹,并创建 configure-db.sh 和 entrypoint.sh。
configure-db.sh:此脚本等待 SQL Server 准备就绪,然后执行在/docker-entrypoint-initdb.d中找到的任何.sql文件以设置数据库和模式。entrypoint.sh:这是 SQL Server 容器的主要入口点。它在后台启动configure-db.sh脚本,然后启动 SQL Server 进程本身。
确保两个文件都使用 LF (Line Feed) 终止符,并在 Linux 中标记为可执行。
在 configure-db.sh 中粘贴以下代码:
#!/bin/bash
# set -x
# 改编自:https://github.com/microsoft/mssql-docker/blob/80e2a51d0eb1693f2de014fb26d4a414f5a5add5/linux/preview/examples/mssql-customize/configure-db.sh
# 等待 60 秒让 SQL Server 启动,方法是确保
# 调用 SQLCMD 不会返回错误代码,这将确保 sqlcmd 是可访问的
# 并且系统和用户数据库返回“0”,这意味着所有数据库都处于“联机”状态
# https://docs.microsoft.com/en-us/sql/relational-databases/system-catalog-views/sys-databases-transact-sql?view=sql-server-2017
dbstatus=1
errcode=1
start_time=$SECONDS
end_by=$((start_time + 60))
echo "Starting check for SQL Server start-up at $start_time, will end at $end_by"
while [[ $SECONDS -lt $end_by && ( $errcode -ne 0 || ( -z "$dbstatus" || $dbstatus -ne 0 ) ) ]]; do
dbstatus="$(/opt/mssql-tools18/bin/sqlcmd -h -1 -t 1 -U sa -P "$MSSQL_SA_PASSWORD" -C -Q "SET NOCOUNT ON; Select SUM(state) from sys.databases")"
errcode=$?
sleep 1
done
elapsed_time=$((SECONDS - start_time))
echo "Stopped checking for SQL Server start-up after $elapsed_time seconds (dbstatus=$dbstatus,errcode=$errcode,seconds=$SECONDS)"
if [[ $dbstatus -ne 0 ]] || [[ $errcode -ne 0 ]]; then
echo "SQL Server took more than 60 seconds to start up or one or more databases are not in an ONLINE state"
echo "dbstatus = $dbstatus"
echo "errcode = $errcode"
exit 1
fi
# 循环遍历 /docker-entrypoint-initdb.d 的根目录中的 .sql 文件并使用 sqlcmd 执行它们
for f in $(find /docker-entrypoint-initdb.d -maxdepth 1 -type f -name "*.sql" | sort); do
echo "- A -=- Processing $f file..."
/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$MSSQL_SA_PASSWORD" -C -d master -i "$f"
done
# 循环遍历 /docker-entrypoint-initdb.d 中的每个子目录
for dir in $(find /docker-entrypoint-initdb.d -mindepth 1 -maxdepth 1 -type d | sort); do
# 循环遍历每个子目录中的 .sql 文件并使用 sqlcmd 执行它们
for f in $(find "$dir" -maxdepth 1 -type f -name "*.sql" | sort); do
echo "- B -=- Processing $f file in directory $dir..."
/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$MSSQL_SA_PASSWORD" -C -d master -i "$f"
done
done
在 entrypoint.sh 中粘贴以下代码:
#!/bin/bash
# 启动脚本以创建数据库和用户
/usr/config/configure-db.sh &
# 启动 SQL Server
/opt/mssql/bin/sqlservr
在你的解决方案的根文件夹中,创建一个 db-scripts 文件夹并添加一个 warehousedb.sql 文件,如下所示:
-- 创建仓库数据库和表的 SQL 脚本
USE [master]
GO
CREATE DATABASE [WarehouseDB]
GO
USE [WarehouseDB]
GO
-- 创建 warehouseitems 表
CREATE TABLE WarehouseItems (
ItemID INT PRIMARY KEY IDENTITY(1,1),
ItemName NVARCHAR(100) NOT NULL,
Stock INT NOT NULL,
LastUpdated DATETIME DEFAULT GETDATE()
);
-- 创建 orders 表
CREATE TABLE Orders (
OrderID INT PRIMARY KEY IDENTITY(1,1),
CustomerName NVARCHAR(100) NOT NULL,
ItemID INT NOT NULL,
Quantity INT NOT NULL,
Status NVARCHAR(50) CHECK (Status IN ('Pending', 'Processing', 'Completed')),
OrderDate DATETIME DEFAULT GETDATE(),
LastUpdated DATETIME DEFAULT GETDATE()
);
-- 用于链接到 Warehouse items 的外键
ALTER TABLE Orders
ADD FOREIGN KEY (ItemID) REFERENCES WarehouseDB.dbo.WarehouseItems(ItemID);
-- 通过为平板电脑创建示例处理订单,向订单表中插入一行
INSERT INTO dbo.Orders (CustomerName, ItemID, LastUpdated, OrderDate, Quantity, Status)
SELECT 'John Doe', ItemID, GETDATE(), GETDATE(), 10, 'Processing'
FROM dbo.WarehouseItems
WHERE ItemName = 'Tablet';
-- 向 warehouseitems 中插入示例数据
INSERT INTO WarehouseItems (ItemName, Stock) VALUES
('Laptop', 50),
('Smartphone', 100),
('Tablet', 75);
所有这些脚本都是在运行项目时填充数据库所必需的,并在 Eshop.AppHost/Program.cs 中使用:
sqlServer
.WithBindMount("./sql-server", target: "/usr/config")
.WithBindMount("../../../db-scripts", target: "/docker-entrypoint-initdb.d")
.WithEntrypoint("/usr/config/entrypoint.sh");
现在我们有了一个本地容器化的数据库,我们可以使用 DAB 公开它。DAB 已作为托管集成添加到我们的 AppHost 中,如下所示:
var dab = builder.AddDataAPIBuilder("dab", "../../../dab/dab-config.json").WithReference(sqlDatabase).WaitFor(sqlServer);
请注意,DAB 资源正在引用数据库资源,并正在等待 SQL 服务器资源。这是因为 DAB 需要等待 SQL 服务器可用才能启动,并且它需要引用数据库的连接字符串。这将导致 .NET Aspire 在容器内注入环境变量 ConnectionStrings_WarehouseDB,这是我们在 dab-config.json 中引用的相同变量。
.NET Aspire 社区工具包
AddDataAPIBuilder 方法来自名为 CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder 的包。你可能会注意到这是一个 Aspire 托管集成,但包名以 CommunityToolkit 开头。这是因为此集成不是由官方 .NET Aspire 团队提供的。
.NET Aspire 产品组将其注意力集中在 Aspire 本身的开发和一些核心集成上。然而,他们单靠自己无法跟上分布式应用程序解决方案中可能需要的所有不同资源。这就是为什么社区开始致力于 .NET Aspire 社区工具包。这是一个公共存储库,社区有兴趣扩展 .NET Aspire 的工作,以共享不同的自定义集成,例如我们用于 DAB 的集成。在底层,该集成仅使用 .NET Aspire 提供的标准容器资源类型来定义 DAB 的预配置容器。这与我们之前看到的 Aspire.Hosting.Redis 集成没有太大区别。
从 Visual Studio 运行 Eshop Aspire
如果你一直跟着到这里,你的本地开发机器上也应该有相同的代码。右键单击 Eshop.AppHost 项目并选择 设为启动项目。通过检查 docker 桌面应用程序,确保 Docker 引擎正在运行。按 F5 或 Visual Studio 顶部窗格中的绿色启动按钮来运行你的 Eshop 应用程序。一个控制台窗口被打开,如图2.9所示,其中包含有关应用程序的详细日志以及到 Aspire 仪表板的链接:
浏览器窗口也随控制台窗口中显示的端点 https://localhost:17136 打开,其中显示了资源视图,如图2.10所示:
如果你在 .NET Aspire 页面加载期间进行监控,SQL 容器将首先开始运行,然后是 DAB,然后是 WarehouseAPI 项目,因为我们在代码中使用了 .WaitFor() 方法。WaitFor (依赖对象) 方法等待依赖资源进入运行状态,然后再启动资源。当一个资源应该等到另一个资源已经开始运行时,这很有用。这可以帮助减少由于依赖关系而在本地开发期间日志中出现的错误。让我们在 Visual Studio 中使用 .http 文件通过调用其端点来测试 WarehouseAPI。
在 Visual Studio 中使用 .http 文件验证 API
Visual Studio 团队添加了 .http 文件编辑器支持,这是一种测试 ASP.NET API 项目的便捷方法,可以直接在 IDE 中发送 HTTP 请求,而无需像 postman、insomnia 或 curl 这样的外部工具。一个文件可以通过使用带有 ### 作为分隔符的行来包含多个请求。以下示例显示了对 https://localhost:7262/api/warehousestatus 端点的 GET 请求:
@WarehouseAPI_HostAddress = https://localhost:7262
GET {{WarehouseAPI_HostAddress}}/api/warehousestatus
Accept: application/json
通过单击 发送,请求将发出一个 HTTP 请求,并且响应将作为 JSON 在侧窗格中返回,如图2.11所示:
结论
在本章中,我们探讨了 .NET Aspire 集成及其好处。我们使用代码文件解释了如何使用预定义的 .NET Aspire 项目模板创建 .NET Aspire 空解决方案并添加我们的第一个 .NET Web API 项目 (WarehouseAPI) 的分步说明。我们简要讨论了 DAB 以及如何在我们的 API 代码中安装和使用它们,以利用与数据库的对话而无需编写大量代码。
在下一章中,我们将讨论如何利用多语言支持(例如,Python、Golang、Node.js)来编写不同的服务,以及 .NET Aspire 如何帮助集成并将它们置于一个保护伞下,从而可以轻松地在本地开发中监控、运行和调试这些微服务。