.NET 10 + DynamoDB 的 REST CRUD API 实战:Serverless Framework 示例 aws-dotnet-rest-api-with-dynamodb 全解析

原创2026-10-13 14:37:551,399 阅读
文章标签:示例工程

.NET 10 + DynamoDB 的 REST CRUD API 实战:Serverless Framework 示例 aws-dotnet-rest-api-with-dynamodb 全解析

本篇技术指南以 Serverless Framework 官方示例仓库中的 aws-dotnet-rest-api-with-dynamodb 为例,讲解如何在 AWS Lambda 的 dotnet10(arm64)托管运行时上构建一个由 DynamoDB 支撑的 CRUD REST API。读完后,你将掌握 .NET 10 项目结构的组织方式、env.configs.yml 本地配置的创建方法、dotnet lambda package 打包流程,以及基于 MediatR + 依赖注入的 Lambda 请求处理链路,能够完整复制该示例的“配置 → 构建 → 测试 → 部署 → 调用”全流程。

项目定位与整体结构

该示例实现了一个由 DynamoDB 支撑的 CRUD REST API(创建、查询、更新),构建于 .NET 10 之上,运行在 dotnet10(arm64)托管运行时。整个解决方案由一个解决方案文件和 src、tests 两级目录组织而成:

├── DotNetServerless.sln
├── src
│   ├── DotNetServerless.Application   # 领域层:实体、Handlers、DynamoDB 仓储
│   └── DotNetServerless.Lambda        # Lambda 入口 + serverless.yml
└── tests
    └── DotNetServerless.Tests

从源码结构看,这是一个典型的分层架构:DotNetServerless.Application 项目(DotNetServerless.Application.csproj)承载实体(Entities)、MediatR 请求处理器(Handlers)、请求/响应模型(Requests/Responses)以及 DynamoDB 仓储(Infrastructure);DotNetServerless.Lambda 项目(DotNetServerless.Lambda.csproj)只负责 Lambda 入口函数、DI 容器装配与 Serverless 部署配置;测试项目 DotNetServerless.Tests 则使用 xunit + Moq 对入口函数做单元测试。

两个项目均 targeting net10.0。Application 项目依赖 AWSSDK.DynamoDBv2、MediatR 12.5.0 与 Newtonsoft.Json;Lambda 项目则额外引入 Amazon.Lambda.APIGatewayEvents、Amazon.Lambda.Serialization.SystemTextJson 等包。值得注意的是 DotNetServerless.Lambda.csproj 中的一行注释:MediatR pinned <13 (Apache-2.0); v13+ requires a commercial license,即 MediatR 被刻意锁定在 12.x 版本以避免 v13+ 的商业授权要求。

使用 env.configs.yml 配置服务

该服务通过一个本地 env.configs.yml 文件进行配置。该文件已被 git 忽略,在打包或部署之前必须在本地创建:

feature: <feature_name>
version: 1.0.0.0
region: <aws_region>
environment: <environment>
dynamoTable: <dynamo_table_name>

最快生成方式:cp env.configs.example.yml env.configs.yml,然后编辑占位值。仓库中提供了示例文件 env.configs.example.yml:

feature: dotnetserverless
version: 1.0.0.0
region: us-east-1
environment: dev
dynamoTable: dotnetserverless-items

需要强调:serverless print / package / deploy 在该文件缺失时都会失败。这不是可选配置,而是硬性前置条件,原因在于 serverless.yml 大量使用了 file() 变量解析:

  • 第 2 行 service: ${file(env.configs.yml):feature} —— 服务名直接取自 feature 字段;
  • 第 8 行 stackName: ${file(env.configs.yml):feature}-${file(env.configs.yml):environment} —— 栈名由 feature 与 environment 拼接;
  • 第 11 行 region: ${file(env.configs.yml):region} —— 部署区域;
  • 第 13 行 DynamoDbConfiguration__TableName: ${file(env.configs.yml):dynamoTable} —— 表名被注入为 Lambda 的环境变量。

也就是说,feature 决定服务名与栈名前缀,environment 区分多环境部署,region 决定资源落点,dynamoTable 同时驱动 IAM 策略的 Resource ARN 与 CloudFormation 表资源(后文详述)。version 字段目前仅用于版本标识,未被 serverless.yml 引用。

此外,serverless.yml 中还有两处关键基础设施定义:

  1. IAM 角色:为执行角色授予对目标表的 dynamodb:* 权限,Resource 精确限定为 arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.DynamoDbConfiguration__TableName},即权限范围与该表一一对应,而非全局表资源。
  2. DynamoDB 表资源(CloudFormation 内联资源 ItemsDynamoDbTable):
    • 主键 Id(HASH,字符串)+ 范围键 Code(RANGE,字符串);
    • 预置容量 ReadCapacityUnits: 1 / WriteCapacityUnits: 1;
    • TableName 取自上面注入的环境变量;
    • DeletionPolicy: Retain —— 删除 CloudFormation 栈时表会被保留,防止误删业务数据。

构建流程:从 npm scripts 到 dotnet lambda package

构建要求安装 .NET 10 SDK 和 Amazon.Lambda.Tools 本地工具(通过签入的 .config/dotnet-tools.json 清单自动还原)。.config/dotnet-tools.json 中锁定 amazon.lambda.tools 版本为 7.0.0,命令名为 dotnet-lambda,这保证了团队成员与 CI 使用同一版本的打包工具。

package.json 提供了三个 npm 脚本,以下所有命令均在 src/DotNetServerless.Lambda 目录下执行:

cd src/DotNetServerless.Lambda

npm run build   # dotnet tool restore && dotnet restore && dotnet lambda package (arm64)
npm run test    # dotnet test
npm run deploy  # build + test + serverless deploy

npm run build 等价于:

dotnet tool restore
dotnet restore
dotnet lambda package --configuration release --framework net10.0 --function-architecture arm64 \
  --output-package bin/release/net10.0/deploy-package.zip

其产物 bin/release/net10.0/deploy-package.zip 正是 serverless.yml 中 package.artifact 所引用的部署包。三个参数的含义:--configuration release 使用发布配置;--framework net10.0 指定目标框架;--function-architecture arm64 指定 ARM 架构——与 provider.architecture: arm64 必须一致。npm run test 实际执行 dotnet test ../../tests/DotNetServerless.Tests/DotNetServerless.Tests.csproj,将 Lambda 项目与独立测试项目关联起来。

部署与 REST 端点调用

部署只需一条命令:

npm run deploy

它会依次执行 build、test、serverless deploy --verbose。部署成功后,API Gateway 提供三个端点(<api-id> 与 <region> 需替换为实际值):

# Create an item
curl --request POST \
  --url https://<api-id>.execute-api.<region>.amazonaws.com/dev/items \
  --header 'Content-Type: application/json' \
  --data '{"Code": "item-code", "Description": "My item", "IsChecked": false}'

# Get an item
curl --request GET \
  --url https://<api-id>.execute-api.<region>.amazonaws.com/dev/items/{id}

# Update an item
curl --request PUT \
  --url https://<api-id>.execute-api.<region>.amazonaws.com/dev/items \
  --header 'Content-Type: application/json' \
  --data '{"Id": "<id>", "Code": "item-code", "Description": "Updated item", "IsChecked": true}'

URL 中的 dev 是 API Gateway 的阶段(stage),与 env.configs.yml 中 environment: dev 对应。

源码深入:请求处理链路与依赖注入

serverless.yml 与 Lambda 函数的映射关系

serverless.yml 定义了三个函数,每个函数对应一个 HTTP 事件:

函数名 Handler HTTP 路由 方法
create DotNetServerless.Lambda.Functions.CreateItemFunction::Run items POST
get DotNetServerless.Lambda.Functions.GetItemFunction::Run items/{id} GET
update DotNetServerless.Lambda.Functions.UpdateItemFunction::Run items PUT

三个路由均开启了 cors: true。runtime: dotnet10 + architecture: arm64 声明了托管运行时,package.artifact 指向构建产物 bin/release/net10.0/deploy-package.zip。

函数入口:MediatR 统一分发

以 CreateItemFunction.cs 为例,处理链路为:

[LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
public async Task<APIGatewayProxyResponse> Run(APIGatewayProxyRequest request)
{
  var requestModel = JsonConvert.DeserializeObject<CreateItemRequest>(request.Body);
  var mediator = _serviceProvider.GetService<IMediator>();
  var result = await mediator.Send(requestModel);
  return new APIGatewayProxyResponse { StatusCode = 201, Body = JsonConvert.SerializeObject(result) };
}

三个入口函数的模式高度一致:把 API Gateway 请求体反序列化为各自的 Request 对象(CreateItemRequest / UpdateItemRequest / GetItemRequest),再通过 MediatR 的 IMediator.Send 分发给 Application 层对应的 Handler,最后根据结果返回 APIGatewayProxyResponse:

  • CreateItemFunction 固定返回 201;
  • GetItemFunction(GetItemFunction.cs)在查不到记录时返回 404,命中时返回 200;值得注意的是它从路径参数构造 new Guid(request.PathParameters["id"]),这意味着 {id} 必须是合法的 GUID 字符串,否则反序列化会直接抛异常;
  • UpdateItemFunction 固定返回 200。

每个函数都提供两个构造函数:无参构造函数调用 Startup.BuildContainer().BuildServiceProvider() 构建 DI 容器(供 Lambda 运行时使用),带 IServiceProvider 参数的构造函数供单元测试注入 mock 容器。

Startup:环境变量驱动的配置装配

Startup.cs 是理解配置如何从 env.configs.yml 一路流入 Lambda 运行时逻辑的关键。它通过 ConfigurationBuilder().AddEnvironmentVariables() 只从环境变量读取配置(env.configs.yml 本身并不打进部署包,而是由 serverless.yml 的变量解析提前转换为 DynamoDbConfiguration__TableName 等环境变量),随后:

services
  .AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining<CreateItemHandler>())
  .AddTransient(typeof(IAwsClientFactory<>), typeof(AwsClientFactory<>))
  .AddTransient<IItemRepository, ItemDynamoRepository>()
  .BindAndConfigure(configurationRoot.GetSection("DynamoDbConfiguration"), new DynamoDbConfiguration())
  .BindAndConfigure(configurationRoot.GetSection("AwsBasicConfiguration"), new AwsBasicConfiguration());

这里有两点值得展开:

  1. MediatR 注册:RegisterServicesFromAssemblyContaining<CreateItemHandler>() 自动扫描 Application 程序集,注册全部 IRequestHandler 实现(CreateItemHandler、GetItemHandler、UpdateItemHandler),新增业务命令无需手工登记。
  2. BindAndConfigure 扩展(ServicesExtensions.cs):先把指定配置节绑定到一个配置对象上,再以单例形式注册。DynamoDbConfiguration 只有一个 TableName 属性,因此环境变量 DynamoDbConfiguration__TableName 中的双下划线 __ 即对应配置节 DynamoDbConfiguration 下的键 TableName;AwsBasicConfiguration 则额外支持通过 AwsBasicConfiguration__AccessKey / __SecretKey / __Region 注入静态凭据。

实体与 DynamoDB 操作

Item.cs 使用 DynamoDB 数据模型特性声明表结构:

[DynamoDBHashKey]
public string Id { get; set; }
[DynamoDBRangeKey]
public string Code { get; set; }
[DynamoDBProperty]
public string Description { get; set; }
[DynamoDBProperty]
public bool IsChecked { get; set; }

Id(HASH)+ Code(RANGE)的组合与 serverless.yml 中 CloudFormation 表的 KeySchema 完全对应。CreateItemHandler 在保存前为 Id 赋值 Guid.NewGuid().ToString(),因此 Create 请求体只需包含 Code、Description、IsChecked 三个字段,Code 由调用方提供并作为范围键参与主键;UpdateItemRequest 则额外携带 Id,用于定位既有记录。

ItemDynamoRepository.cs 基于 DynamoDBContext 实现了两种操作:

  • Save:调用 context.SaveAsync(item, _configuration, cancellationToken) 写入实体;
  • GetById:用 ScanCondition 按 Id 相等条件执行 ScanAsync<T>,并循环 GetNextSetAsync 直到分页结束。

仓储构造函数中设置了 DynamoDBOperationConfig { OverrideTableName = configuration.TableName, SkipVersionCheck = true }:OverrideTableName 使表名完全来自配置(即 DynamoDbConfiguration__TableName),SkipVersionCheck 关闭 DynamoDB 数据模型的全局版本校验开销。从源码结构看,GetById 使用的是 Scan 而非 GetItem/Query,在数据量大时成本会高于按主键定位,这是一个可以留意的实现细节。

AWS 客户端工厂:凭据的两种来源

AWSClientFactory.cs 是仓储获取 AmazonDynamoDBClient 的唯一途径,其逻辑为:当 AwsBasicConfiguration 中的 AccessKey/SecretKey 为空时,用无参构造创建客户端(走 AWS 默认凭据链——在 Lambda 中即执行角色的临时凭据);否则使用显式凭据与 RegionEndpoint 创建客户端。也就是说:生产部署时依赖 IAM 执行角色即可,静态凭据配置主要用于本地调试场景。AwsBasicConfiguration.RegionEndpoint 通过 RegionEndpoint.EnumerableAllRegions 按名称匹配解析,未匹配到时返回 null。

单元测试:不触网的函数级验证

测试项目 DotNetServerless.Tests.csproj 使用 xunit 2.9.3 与 Moq 4.20.72。以 CreateItemFunctionTests.cs 为例,测试策略是:

  1. 用 Mock<IItemRepository> 模拟仓储;
  2. 复用生产装配入口 Startup.BuildContainer(),再通过 serviceCollection.Replace(...) 将 IItemRepository 替换为 mock——这验证了带 IServiceProvider 参数的构造函数注入点在测试中是可用的;
  3. 构造 APIGatewayProxyRequest 直接调用 Run,断言 Save 恰好被调用一次、返回状态码为 201。

GetItemFunctionTests.cs 进一步覆盖了 200(命中)与 404(未命中)两种分支;UpdateItemFunctionTests.cs 结构类似。整套测试完全离线运行,npm run deploy 中的 npm run test 步骤就是这套离线回归,保证部署前行为已被验证。

关键文件索引

文件 作用
aws-dotnet-rest-api-with-dynamodb/README.md 示例总文档(配置/构建/部署/调用)
aws-dotnet-rest-api-with-dynamodb/src/DotNetServerless.Lambda/serverless.yml 函数、路由、IAM、DynamoDB 表定义
aws-dotnet-rest-api-with-dynamodb/src/DotNetServerless.Lambda/env.configs.example.yml 本地配置模板
aws-dotnet-rest-api-with-dynamodb/src/DotNetServerless.Lambda/package.json build / test / deploy 脚本
aws-dotnet-rest-api-with-dynamodb/src/DotNetServerless.Lambda/.config/dotnet-tools.json amazon.lambda.tools 7.0.0 工具清单
aws-dotnet-rest-api-with-dynamodb/src/DotNetServerless.Lambda/Startup.cs DI 容器与配置绑定
aws-dotnet-rest-api-with-dynamodb/src/DotNetServerless.Application/Infrastructure/Repositories/ItemDynamoRepository.cs DynamoDB 读写实现
aws-dotnet-rest-api-with-dynamodb/tests/DotNetServerless.Tests/Functions/GetItemFunctionTests.cs 函数级单元测试示例

小结

该示例完整展示了在 Serverless Framework 下以 .NET 10 托管运行时交付 DynamoDB REST API 的标准套路:env.configs.yml 作为本地唯一可变配置源,经 serverless.yml 的 file() 解析转换为服务名、区域与 Lambda 环境变量;dotnet lambda package 产出固定路径的部署包;应用层以 MediatR + DI 组织请求处理,凭据优先走执行角色;xunit + Moq 在不接触 AWS 的前提下验证状态码与仓储调用。若要在自己项目中复用,最小改动路径是:替换 env.configs.yml 中的五个占位值、按 Item 实体的 HASH/RANGE 组合调整表结构,再按 serverless.yml 的 functions 段扩展新的路由与对应 Function/Handler。

登录后查看全文
examples