在Linux上为.NET Aspire添加DELETE端点
[[academy-video-youtube({"vid": "x10CYBXrLxg", "start_time": "0", "title": "在.NET Aspire on Linux中添加DELETE端点", "creator": "Tim Corey", "length": "8m 40s"})]]
每个 CRUD API 最终都需要一种移除记录的方法,而 DELETE 动词则完成了与 GET、POST 和 PUT 一起四个核心 HTTP 操作。 与其他动词相比,DELETE 在结构上最简单:没有请求主体、没有验证管道、没有复杂的返回类型。 它引入的问题是设计上的,这没有普遍正确的答案,即当调用者要求删除不存在的记录时应该返回什么。
在他的视频《在 Linux 上的 .NET Aspire 中添加 DELETE 端点》中,Tim Corey 完成了 Tiny Ticket API 添加最后一个端점을,修正了在 GET-by-ID 存储过程中静默存在的参数大小写不一致,并讨论何时返回 404 还是 204 对于缺失的记录。 这一集还预览了向前端的转变,这将成为 C# on Linux 系列下一个阶段的焦点。 如果您一直在关注本系列或第一次在最小 API 上连接 DELETE,本文将完整介绍端点和使参数绑定在项目中保持一致的小重构。
映射 DELETE 端点
[1:02 - 2:14] 端点注册遵循与其他路由相同的形状,但有两个调整。 该路由包含一个MapPut。 没有输入记录,因为除了标识符之外,不需要其他任何东西。
app.MapDelete("/api/tickets/{id:int}",
async Task<Results<NoContent, ValidationProblem>>
(ISqlDataAccess sql, int id) =>
{
await sql.SaveDataAsync("dbo.spTickets_Delete",
new { Id = id }, "TicketDB");
return TypedResults.NoContent();
});app.MapDelete("/api/tickets/{id:int}",
async Task<Results<NoContent, ValidationProblem>>
(ISqlDataAccess sql, int id) =>
{
await sql.SaveDataAsync("dbo.spTickets_Delete",
new { Id = id }, "TicketDB");
return TypedResults.NoContent();
});处理程序通过Dapper包装器调用spTickets_Delete存储过程,传递一个带有ID的匿名对象。 返回TypedResults.NoContent()会产生204状态,表示操作成功且没有响应主体可以返回。 返回类型声明镜像上一个剧集的 PUT 端点,因为从框架的角度来看,两者都有相同的可能结果集合。
修复参数大小写不匹配
[2:14 - 4:32] 在连接 DELETE 调用时,Tim 注意到他在系列早期引入的不一致性。 Id = id赋值。 Id。 但用于通过ID获取功能的GET端点背后的id。 该小写变体让原始处理程序可以不显式赋值地传递new { id },当时感觉很方便,但使代码库不一致。
为了不延续这种不对称性,他打开SQL Server Management Studio并更改GET过程以使用大写的Id:
ALTER PROCEDURE spTickets_Get
@Id int
AS
BEGIN
SELECT Id, Title, Description, DateCompleted, Priority, CreatedDate
FROM dbo.Tickets
WHERE Id = @Id;
END随着过程更新,Program.cs中的GET处理程序现在需要与DELETE和PUT处理程序相同的显式映射,从new { Id = id }。 更改是机械的,但推理很重要:存储过程之间一致的参数大小写意味着每个端点以相同的方式绑定参数,这在稍后阅读数据访问层时消除了一个小但真实的困惑来源。 仅在四个地方之一适用的约定并不是约定。
何时对缺失的记录返回 204 vs. 404
[4:46 - 5:46] 端点编译后,Tim 对每个 DELETE 实现中都会遇到的设计问题暂停。 如果调用者传递了不存在的 ID,API 应该返回什么? 有两个合理的答案。
无论是否删除行,返回 204 NoContent 将请求视为幂等的。 从调用者的角度来看,资源已经不在了,这就是目标。 这是当前处理程序的工作方式,也是 Tiny Ticket 项目将提供的内容。 返回 404 NotFound 对于缺失的记录会给调用者提供更多的信息,但需要存储过程报告是否实际删除了一行,通常是通过返回处理程序可以检查的行计数,以决定要发送哪个响应。
对于前端已经知道哪些 ID 存在的内部 CRUD API(因为它刚刚加载了列表),204 就可以了。对于可能猜测 ID 的公共 API,404 防止了数据被移除但实际上从未存在于无声的幻觉。 Tim 注意到返回 404 可能泄露数据库中哪些 ID 存在的信息,尽管对于删除操作来说,实际风险很低,因为行使端点已经意味着写入访问。
通过 Swagger 进行测试
[5:46 - 7:08] 随着数据库的运行,Tim 启动 API 并打开Swagger。 他从 Get all 开始以了解当前数据:来自原始种子的记录 1、2、3,加上来自早期插入测试的 107、109 和 110。
他对 107 执行 DELETE 并返回 204。110 也是如此。为了验证缺失记录的行为,他在 1011 上运行 DELETE,这是一个数据库中从未有过的 ID。 响应仍然是 204,没有任何迹象表明什么都没被删除。 这就是上一部分讨论的权衡,现在在实际的 API 响应中可见。
第二次 Get all 确认了最终状态:记录 1、2、3 和 109 仍然存在。 对于有效的 ID,DELETE 端点正常工作,而对于无效的 ID 则默默失败,正如实现所规定的那样。
总结:CRUD 完成
[7:08 - 8:38] 添加 DELETE 完成了 Tiny Ticket API 的四个 CRUD 动词。 相同的结构模式在每个端点中体现:路由定义、存储过程名称、Dapper 数据访问调用、类型化结果。 Tim 坦率地指出,生产 API 可能会添加更多的端点,例如用于标记票务完成而不通过无线发送整个对象的 PATCH 或专用于搜索的端点。 然而,系列的目标是使每一层的焦点保持一致,以便下一个层(前端)具有清晰的调用界面。
一致性使项目易于阅读。 Dapper 包装器、POST 插入模式、验证管道和类型化结果相结合,使每一个新端点无论实现哪个动词都需大致相同代码量。 这种可预测性使 API 成为接下来前端工作的舒适目标。
结论
[8:38 - 8:40] 添加DELETE端点到一个最小API需要一个带有ID段的路由的TypedResults.NoContent()。 该端点完成了 Tiny Ticket 项目的 CRUD 表面并设置了系列的下一阶段的前端转向。
系列导航:本文是 C# on Linux 系列的一部分,构建 Tiny Ticket 应用程序。上一个:添加 PUT 更新端点。 下一阶段:调用 API 的前端页面。
示例提示:如果您想要更加信息丰富的DELETE响应而不改变存储过程,捕获受影响的行数来自TypedResults.NotFound()。 这在不重构数据访问层的情况下添加了 404 路径。

