在Linux上为.NET Aspire添加按ID获取端点
[[academy-video-youtube({"vid": "hMhA5sLHUpY", "start_time": "0", "title": "在.NET Aspire on Linux中添加通过ID获取的端点", "creator": "Tim Corey", "length": "15m 20s"})]]
返回数据库表中的每一条记录对于列表页面很有用,但大多数 API 用户也需要按标识符提取单个记录。 那个第二个端点引入了"全取"路由不需要的决策:哪个返回类型对单个对象与集合更合适,当 ID 与任何记录不匹配时会发生什么,以及如何通过正确的 HTTP 状态码将该失败传达给调用者。
在他的视频"在Linux上的.NET Aspire中添加按ID获取的端点"中,Tim Corey通过添加GET /api/tickets/{id}端点继续Tiny Ticket API。 开始时复制现有的路由,然后变成实时演示边缘案例处理:返回一个单一对象而不是数组,检查空结果,并使用404 Not Found。如果您正在关注在Linux上的C#系列或构建需要正确状态码响应的最小API,这一集涵盖了完整的思考过程。
复制"获取所有"路由作为起点
[[0:35 - 1:45]] Tim打开了API服务的GET /api/tickets端点。 新路由需要一个票据ID的路径参数,因此URL模式更改为包含int id参数。 存储过程引用从spTickets_Get,它需要一个ID参数。
app.MapGet("/api/tickets/{id:int}", async (int id, IDbConnection db) =>
{
var tickets = await db.LoadSqlAsync<TicketModel>("spTickets_Get", new { id });
// Initial version: returns a list, which we'll fix next
return tickets;
});app.MapGet("/api/tickets/{id:int}", async (int id, IDbConnection db) =>
{
var tickets = await db.LoadSqlAsync<TicketModel>("spTickets_Get", new { id });
// Initial version: returns a list, which we'll fix next
return tickets;
});一个值得注意的命名便利:存储过程参数是小写的id,与C#参数名完全匹配。 这意味着Dapper可以直接映射匿名对象new { id },无需指定属性名称。 如果SQL参数使用不同的大小写,匿名对象将需要一个显式的属性分配,如new { Id = id }。
返回单个对象而不是列表
[2:39 - 4:16] 第一个通过Swagger的测试揭示了一个问题:传递ID 2返回一个200状态码和正确的票据,但是响应主体被包装在一个JSON数组中。 当调用者通过ID请求单个资源时,他们期望的是一个单独的对象,而不是包含一个元素的集合。
在查询结果中添加.FirstOrDefault()修复了包装问题。 null。 这解决了数组问题,但引入了一个新问题:当ID不匹配任何记录时,API应该返回什么?
var output = tickets.FirstOrDefault();var output = tickets.FirstOrDefault();这个单行更改为现有记录生成了正确的响应格式。 然而,用ID 4(在数据库中不存在)进行测试时,揭示了一个更深层的问题。响应返回为null,状态码为200。 这在技术上是合法的HTTP,但误导了调用者:200表示请求成功并且找到了资源,而实际上没有任何匹配项。
使用TypedResults处理未找到的情况
[4:41 - 11:44] 这一部分是蒂姆实时处理设计决策的地方,因此比起只阅读最终代码,观看更有价值。 他的思维过程经历了几个迭代:
首先,他考虑使用.FirstOrDefault(),这在列表为空时抛出异常。 这会产生500错误,这比空200更糟,因为500意味着服务器错误而不是资源缺失。
然后他退一步,构建一个空检查。 他将结果存储在一个变量中,检查它是否为空,并为每种情况返回不同的响应。 挑战在于,当一个Minimal API处理程序可以返回多种响应形态时,需要明确声明其返回类型。
解决方案是TypedResults,这允许您在方法签名中指定可能的响应类型:
app.MapGet("/api/tickets/{id:int}", async Task<Results<Ok<TicketModel>, NotFound>> (int id, IDbConnection db) =>
{
var tickets = await db.LoadSqlAsync<TicketModel>("spTickets_Get", new { id });
var output = tickets?.FirstOrDefault();
if (output is null)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(output);
});app.MapGet("/api/tickets/{id:int}", async Task<Results<Ok<TicketModel>, NotFound>> (int id, IDbConnection db) =>
{
var tickets = await db.LoadSqlAsync<TicketModel>("spTickets_Get", new { id });
var output = tickets?.FirstOrDefault();
if (output is null)
{
return TypedResults.NotFound();
}
return TypedResults.Ok(output);
});该返回类型,TicketModel内容的200或没有内容的404。 VS Code中的彩色括号匹配帮助导航快速堆积的泛型结果类型中的嵌套角括号。
测试期间出现一个细微的错误:第一个版本调用return它。 端点可以编译,因为Ok路径。 Tim在Swagger仍然显示缺失ID的200时发现了这个问题,添加了return,下一次运行时404正确显示。
在Swagger中测试两个路径
[11:44 - 14:09] 用最终代码,Tim在Swagger UI中运行两个场景。 传递ID 3返回一个200和票据对象。 传递ID 4返回一个404和空响应体。
他还指出Swagger界面中的一个细节可能会让第一次用户困惑:执行按钮下方的"Responses"部分显示可能的响应代码(200和404),而不是实际结果。 实际的服务器响应出现在该部分上方的一个独立面板中。 混淆这两个面板是导致"为什么我得到200?"困惑的常见原因。
TypedResults方法也自动改善了Swagger文档。 因为返回类型同时声明了NotFound,Swagger展示两者为可能的结果及其各自的模式。 读API文档的调用者知道他们需要处理404案例,而无需开发者编写单独的OpenAPI注释。
总结:在功能之前处理边缘案例
[14:09 - 15:09] 从简简单单的复制粘贴get-all端点开始演变成API设计的深入练习。 最终版本处理了理想路径(找到记录),预期的失败(未找到记录)和防御性空检查以应对意想不到的情况(查询返回空)。 Tim通过现场处理这些案例,而不是展示精致代码,展示了生产端点所需的迭代思考方式。
结论
[[15:09 - 15:20]] 向一个最小API添加按ID获取端点涉及除路线定义之外的三个决定:使用TypedResults以便框架返回正确的HTTP状态码。 Results<Ok<t>, NotFound>模式可以在任何需要传达成功或不存在的端点之间重用。
系列导航:本文是Tiny Ticket应用建设过程中Linux上的C#系列的一部分。上一个:添加Swagger UI。 下一个:添加POST插入端点。
示例提示:当您的最小API处理程序返回多个可能的状态码时,始终在Results<>泛型中声明它们。 这会自动生成准确的Swagger文档,并强制编译器验证每个代码路径返回一个有效的结果类型。

