关于 minimal-apis
Minimal APIs 是一项开发技能,用于使用 ASP.NET Core 构建轻量 HTTP API 和微服务。内容涵盖端点组织、验证、类型化响应和 OpenAPI 文档。
主要功能
- 01使用 Minimal APIs 创建端点
- 02分组路由并共享配置
- 03用端点筛选器执行验证
- 04使用 TypedResults 提供类型化响应
- 05生成 OpenAPI 文档
适用场景
- 01构建轻量 HTTP API
- 02组织相关服务端点
- 03验证传入请求
- 04用 OpenAPI 编写 API 文档
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ managedcode/ dotnet-skills --skill minimal-apis安装后在 Claude Code 中输入/minimal-apis确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
manifest.jsonreferences/anti-patterns.mdpatterns.md详细介绍
Minimal API
适用场景
- 在 ASP.NET Core 中构建新的 HTTP API
- 创建轻量级微服务
- 在 Minimal API 和控制器之间做出选择
- 使用路由组组织端点
- 实现验证和筛选器
文档
参考资料
- patterns.md - 详细介绍路由组、筛选器、TypedResults 模式、参数绑定、错误处理和测试
- anti-patterns.md - 应避免的常见 Minimal API 错误
何时使用 Minimal API,何时使用控制器
| 使用 Minimal API | 使用控制器 |
|---|---|
| 新项目 | 现有 MVC/API 项目 |
| 微服务 | 复杂的模型绑定 |
| 简单的 CRUD API | OData、JsonPatch |
| 轻量级处理程序 | 大量使用特性 |
| .NET 8+ 项目 | 需要 [ApiController] 功能 |
工作流
- 直接在 Program.cs 中定义端点(适用于小型 API)
- 使用路由组组织相关端点
- 随着 API 增长,将处理程序移至单独的类
- 应用筛选器处理横切关注点
- 使用 TypedResults实现类型安全的响应
- 使用
.WithOpenApi()生成 OpenAPI 文档
上游最新说明
dotnet/aspnetcore的v10.0.11是维护版本;它不会更改 Minimal API 的路由组、筛选器或TypedResults模型,但会将 OpenAPI 技术栈更新到2.7.5。升级后,请重新生成文档并检查契约快照。- 2026 年 8 月的
aspnetcore-10.0概述仍将轻量级 HTTP API 指向此处。需要了解精确的 OpenAPI、筛选器或参数绑定行为时,请使用专门的 Minimal API 页面。
基本模式
简单端点
var app = builder.Build();
app.MapGet("/", () => "Hello World");
app.MapGet("/products/{id}", (int id) => Results.Ok(new { Id = id }));
app.MapPost("/products", (Product product) => Results.Created($"/products/{product.Id}", product));
TypedResults(强类型)
app.MapGet("/products/{id}", Results<Ok<Product>, NotFound> (int id, AppDb db) =>
{
var product = db.Products.Find(id);
return product is not null
? TypedResults.Ok(product)
: TypedResults.NotFound();
});
依赖注入
app.MapGet("/products", async (IProductService service) =>
{
return await service.GetAllAsync();
});
// Or with [FromServices] for clarity
app.MapGet("/products", async ([FromServices] IProductService service) =>
await service.GetAllAsync());
路由组
基本分组
var products = app.MapGroup("/api/products");
products.MapGet("/", GetAll);
products.MapGet("/{id}", GetById);
products.MapPost("/", Create);
products.MapPut("/{id}", Update);
products.MapDelete("/{id}", Delete);
具有共享配置的组
var api = app.MapGroup("/api")
.RequireAuthorization()
.AddEndpointFilter<ValidationFilter>();
var products = api.MapGroup("/products")
.WithTags("Products");
var orders = api.MapGroup("/orders")
.WithTags("Orders")
.RequireAuthorization("AdminOnly");
端点筛选器
内联筛选器
app.MapGet("/products/{id}", (int id) => Results.Ok(id))
.AddEndpointFilter(async (context, next) =>
{
var id = context.GetArgument<int>(0);
if (id <= 0)
return Results.BadRequest("Invalid ID");
return await next(context);
});
基于类的筛选器
public class ValidationFilter<T> : IEndpointFilter where T : class
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var argument = context.Arguments
.OfType<T>()
.FirstOrDefault();
if (argument is null)
return Results.BadRequest("Invalid request body");
var validator = context.HttpContext.RequestServices
.GetService<IValidator<T>>();
if (validator is not null)
{
var result = await validator.ValidateAsync(argument);
if (!result.IsValid)
return Results.ValidationProblem(result.ToDictionary());
}
return await next(context);
}
}
// Usage
products.MapPost("/", Create)
.AddEndpointFilter<ValidationFilter<CreateProductRequest>>();
通过根组配置全局筛选器
// All endpoints inherit filters from root group
var root = app.MapGroup("")
.AddEndpointFilter<LoggingFilter>()
.AddEndpointFilter<ErrorHandlingFilter>();
root.MapGet("/health", () => Results.Ok());
root.MapGroup("/api/products").MapGet("/", GetProducts);
组织较大型 API
扩展方法模式
// ProductEndpoints.cs
public static class ProductEndpoints
{
public static RouteGroupBuilder MapProductEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/products")
.WithTags("Products");
group.MapGet("/", GetAll);
group.MapGet("/{id}", GetById);
group.MapPost("/", Create);
return group;
}
private static async Task<Ok<List<Product>>> GetAll(IProductService service)
=> TypedResults.Ok(await service.GetAllAsync());
private static async Task<Results<Ok<Product>, NotFound>> GetById(
int id, IProductService service)
{
var product = await service.GetByIdAsync(id);
return product is not null
? TypedResults.Ok(product)
: TypedResults.NotFound();
}
private static async Task<Created<Product>> Create(
CreateProductRequest request, IProductService service)
{
var product = await service.CreateAsync(request);
return TypedResults.Created($"/api/products/{product.Id}", product);
}
}
// Program.cs
app.MapProductEndpoints();
app.MapOrderEndpoints();
请求/响应 DTO
// Separate from domain models
public record CreateProductRequest(string Name, decimal Price);
public record UpdateProductRequest(string Name, decimal Price);
public record ProductResponse(int Id, string Name, decimal Price);
// Don't expose domain entities directly
app.MapPost("/products", (CreateProductRequest request, IMapper mapper) =>
{
var product = mapper.Map<Product>(request);
// ...
return TypedResults.Created($"/products/{product.Id}",
mapper.Map<ProductResponse>(product));
});
应避免的反模式
| 反模式 | 问题所在 | 更好的做法 |
|---|---|---|
| 所有内容都放在 Program.cs 中 | 难以维护 | 使用扩展方法 |
| 不使用路由组 | 配置重复 | 将相关端点分组 |
| 手动验证 | 容易出错 | 使用筛选器 + FluentValidation |
| 暴露实体 | 耦合紧密 | 使用 DTO |
| 不使用 TypedResults | 缺少编译时检查 | 使用 TypedResults |
| 忽略 OpenAPI | 没有文档 | 添加 .WithOpenApi() |
OpenAPI 集成
builder.Services.AddOpenApi();
app.MapOpenApi(); // Serves OpenAPI spec
app.MapGet("/products", GetProducts)
.WithName("GetProducts")
.WithSummary("Get all products")
.WithDescription("Returns a list of all available products")
.Produces<List<Product>>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status500InternalServerError);
交付内容
- 简洁、组织良好的 Minimal API 端点
- 正确使用路由组和筛选器
- 使用 TypedResults 返回类型安全的响应
- OpenAPI 文档
- 使用端点筛选器进行验证
验证
- 端点返回正确的状态码
- 验证筛选器能捕获无效输入
- OpenAPI 规范准确无误
- 路由组共享通用配置
- 处理程序可测试(可以模拟依赖项)
评论
支持 Markdown