.NET 10 + DynamoDB 的 REST CRUD API 实战:Serverless Framework 示例 aws-dotnet-rest-api-with-dynamodb 全解析
.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 中还有两处关键基础设施定义:
- IAM 角色:为执行角色授予对目标表的
dynamodb:*权限,Resource 精确限定为arn:aws:dynamodb:${self:provider.region}:*:table/${self:provider.environment.DynamoDbConfiguration__TableName},即权限范围与该表一一对应,而非全局表资源。 - 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());
这里有两点值得展开:
- MediatR 注册:
RegisterServicesFromAssemblyContaining<CreateItemHandler>()自动扫描 Application 程序集,注册全部IRequestHandler实现(CreateItemHandler、GetItemHandler、UpdateItemHandler),新增业务命令无需手工登记。 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 为例,测试策略是:
- 用
Mock<IItemRepository>模拟仓储; - 复用生产装配入口
Startup.BuildContainer(),再通过serviceCollection.Replace(...)将IItemRepository替换为 mock——这验证了带IServiceProvider参数的构造函数注入点在测试中是可用的; - 构造
APIGatewayProxyRequest直接调用Run,断言Save恰好被调用一次、返回状态码为 201。
GetItemFunctionTests.cs 进一步覆盖了 200(命中)与 404(未命中)两种分支;UpdateItemFunctionTests.cs 结构类似。整套测试完全离线运行,npm run deploy 中的 npm run test 步骤就是这套离线回归,保证部署前行为已被验证。
关键文件索引
小结
该示例完整展示了在 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。