关于 dotnet-minimal-apis
面向 .NET 开发者的 ASP.NET Core Minimal API 实践技能,涵盖路由分组、筛选器、类型化响应、参数绑定与 OpenAPI 指南。
主要功能
- 01路由组与嵌套路由
- 02端点筛选器管线
- 03TypedResults 与响应架构
- 04OpenAPI 3.1 集成
- 05参数绑定与 JSON 配置
- 06端点组织模式
适用场景
- 01组织相关 API 端点
- 02应用共享的验证或日志筛选器
- 03为 OpenAPI 生成响应架构
- 04规划不断扩大的 Minimal API 项目
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-minimal-apis安装后在 Claude Code 中输入/dotnet-minimal-apis确认已加载。
详细介绍
dotnet-minimal-apis
Minimal API 是 Microsoft 针对新的 ASP.NET Core HTTP API 项目推荐的方案。它们提供轻量、基于 lambda 的编程模型,原生支持 OpenAPI、用于处理横切关注点的端点筛选器,以及便于大规模组织端点的路由组。
不在本文范围内: API 版本控制策略——请参阅 [skill:dotnet-api-versioning]。输入验证框架和模式——请参阅 [skill:dotnet-input-validation]。架构模式(垂直切片、CQRS、整洁架构)——请参阅 [skill:dotnet-architecture-patterns]。身份验证和授权实现——请参阅 [skill:dotnet-api-security]。OpenAPI 文档生成和自定义——请参阅 [skill:dotnet-openapi]。
交叉引用:[skill:dotnet-architecture-patterns] 介绍大型 API 的组织方式,[skill:dotnet-input-validation] 介绍请求验证,[skill:dotnet-api-versioning] 介绍版本控制策略,[skill:dotnet-openapi] 介绍 OpenAPI 自定义。
路由组
路由组可将相关端点组织在共享前缀下,并一次性应用通用配置(筛选器、元数据、授权)。它们通过共享前缀取代重复的 MapGet/MapPost 链式调用。
var app = builder.Build();
// Group endpoints under /api/products with shared configuration
var products = app.MapGroup("/api/products")
.WithTags("Products")
.RequireAuthorization();
products.MapGet("/", async (AppDbContext db) =>
TypedResults.Ok(await db.Products.ToListAsync()));
products.MapGet("/{id:int}", async (int id, AppDbContext db) =>
await db.Products.FindAsync(id) is Product product
? TypedResults.Ok(product)
: TypedResults.NotFound());
products.MapPost("/", async (CreateProductDto dto, AppDbContext db) =>
{
var product = new Product { Name = dto.Name, Price = dto.Price };
db.Products.Add(product);
await db.SaveChangesAsync();
return TypedResults.Created($"/api/products/{product.Id}", product);
});
products.MapDelete("/{id:int}", async (int id, AppDbContext db) =>
{
if (await db.Products.FindAsync(id) is not Product product)
return TypedResults.NotFound();
db.Products.Remove(product);
await db.SaveChangesAsync();
return TypedResults.NoContent();
});
嵌套路由组
可以嵌套路由组,以组合前缀和筛选器:
var api = app.MapGroup("/api")
.AddEndpointFilter<RequestLoggingFilter>();
var v1 = api.MapGroup("/v1");
var products = v1.MapGroup("/products").WithTags("Products");
var orders = v1.MapGroup("/orders").WithTags("Orders");
// Registers as: GET /api/v1/products
products.MapGet("/", GetProducts);
// Registers as: POST /api/v1/orders
orders.MapPost("/", CreateOrder);
端点筛选器
端点筛选器提供了一个处理横切关注点(日志记录、验证、授权增强)的管道,类似于 MVC 操作筛选器,但专用于 Minimal API。
IEndpointFilter 接口
public sealed class ValidationFilter<T>(IValidator<T> validator) : IEndpointFilter
where T : class
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
// Extract the argument of type T from the endpoint parameters
var argument = context.Arguments
.OfType<T>()
.FirstOrDefault();
if (argument is null)
return TypedResults.BadRequest("Request body is required");
var result = await validator.ValidateAsync(argument);
if (!result.IsValid)
{
return TypedResults.ValidationProblem(
result.ToDictionary());
}
return await next(context);
}
}
应用筛选器
// Apply to a single endpoint
products.MapPost("/", CreateProduct)
.AddEndpointFilter<ValidationFilter<CreateProductDto>>();
// Apply to an entire route group
var products = app.MapGroup("/api/products")
.AddEndpointFilter<RequestLoggingFilter>();
// Inline filter using a lambda
products.MapGet("/{id:int}", GetProductById)
.AddEndpointFilter(async (context, next) =>
{
var id = context.GetArgument<int>(0);
if (id <= 0)
return TypedResults.BadRequest("ID must be positive");
return await next(context);
});
筛选器执行顺序
筛选器按注册顺序执行(最先注册的位于最外层)。所有筛选器均通过后,才会运行端点处理程序:
Request -> Filter1 -> Filter2 -> Filter3 -> Handler
Response <- Filter1 <- Filter2 <- Filter3 <-
TypedResults
Minimal API 的返回值始终使用 TypedResults(静态工厂),而不是 Results(接口工厂)。TypedResults 会返回具体类型,OpenAPI 元数据生成器可以在构建时检查这些类型,从而自动生成准确的响应架构。
// PREFERRED: TypedResults -- concrete return types, auto-generates OpenAPI metadata
products.MapGet("/{id:int}", async Task<Results<Ok<Product>, NotFound>> (
int id, AppDbContext db) =>
await db.Products.FindAsync(id) is Product product
? TypedResults.Ok(product)
: TypedResults.NotFound());
// AVOID: Results -- returns IResult, OpenAPI generator cannot infer response types
products.MapGet("/{id:int}", async (int id, AppDbContext db) =>
await db.Products.FindAsync(id) is Product product
? Results.Ok(product)
: Results.NotFound());
联合返回类型
使用 Results<T1, T2, ...> 声明单个端点的所有可能响应类型。这样可以生成包含多个响应代码的准确 OpenAPI 文档:
products.MapPost("/", async Task<Results<Created<Product>, ValidationProblem, Conflict>> (
CreateProductDto dto, AppDbContext db) =>
{
if (await db.Products.AnyAsync(p => p.Sku == dto.Sku))
return TypedResults.Conflict();
var product = new Product { Name = dto.Name, Sku = dto.Sku, Price = dto.Price };
db.Products.Add(product);
await db.SaveChangesAsync();
return TypedResults.Created($"/api/products/{product.Id}", product);
});
OpenAPI 3.1 集成
.NET 10 通过 Microsoft.AspNetCore.OpenApi 增加了内置的 OpenAPI 3.1 支持。Minimal API 会根据 TypedResults、参数绑定和特性自动生成 OpenAPI 元数据。
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi(); // Serves /openapi/v1.json
}
丰富元数据
products.MapGet("/{id:int}", GetProductById)
.WithName("GetProductById")
.WithSummary("Get a product by its ID")
.WithDescription("Returns the product details for the specified ID, or 404 if not found.")
.Produces<Product>(StatusCodes.Status200OK)
.ProducesProblem(StatusCodes.Status404NotFound);
关于高级 OpenAPI 自定义(文档转换器、操作转换器、架构自定义),请参阅 [skill:dotnet-openapi]。
大规模组织模式
当 API 的端点数量超过少数几个时,应将端点组织到单独的静态类或扩展方法中。
扩展方法模式
// ProductEndpoints.cs
public static class ProductEndpoints
{
public static RouteGroupBuilder MapProductEndpoints(this IEndpointRouteBuilder routes)
{
var group = routes.MapGroup("/api/products")
.WithTags("Products");
group.MapGet("/", GetAll);
group.MapGet("/{id:int}", GetById);
group.MapPost("/", Create);
group.MapPut("/{id:int}", Update);
group.MapDelete("/{id:int}", Delete);
return group;
}
private static async Task<Ok<List<Product>>> GetAll(AppDbContext db) =>
TypedResults.Ok(await db.Products.ToListAsync());
private static async Task<Results<Ok<Product>, NotFound>> GetById(
int id, AppDbContext db) =>
await db.Products.FindAsync(id) is Product p
? TypedResults.Ok(p)
: TypedResults.NotFound();
private static async Task<Created<Product>> Create(
CreateProductDto dto, AppDbContext db)
{
var product = new Product { Name = dto.Name, Price = dto.Price };
db.Products.Add(product);
await db.SaveChangesAsync();
return TypedResults.Created($"/api/products/{product.Id}", product);
}
private static async Task<Results<NoContent, NotFound>> Update(
int id, UpdateProductDto dto, AppDbContext db)
{
var product = await db.Products.FindAsync(id);
if (product is null) return TypedResults.NotFound();
product.Name = dto.Name;
product.Price = dto.Price;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
private static async Task<Results<NoContent, NotFound>> Delete(
int id, AppDbContext db)
{
var product = await db.Products.FindAsync(id);
if (product is null) return TypedResults.NotFound();
db.Products.Remove(product);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
}
// Program.cs
app.MapProductEndpoints();
app.MapOrderEndpoints();
app.MapCustomerEndpoints();
Carter 库
对于倾向于自动发现端点模块的项目,Carter 库提供了 ICarterModule 接口:
// <PackageReference Include="Carter" Version="8.*" />
public sealed class ProductModule : ICarterModule
{
public void AddRoutes(IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/products").WithTags("Products");
group.MapGet("/", async (AppDbContext db) =>
TypedResults.Ok(await db.Products.ToListAsync()));
group.MapGet("/{id:int}", async (int id, AppDbContext db) =>
await db.Products.FindAsync(id) is Product p
? TypedResults.Ok(p)
: TypedResults.NotFound());
}
}
// Program.cs
builder.Services.AddCarter();
var app = builder.Build();
app.MapCarter(); // Auto-discovers and registers all ICarterModule implementations
垂直切片组织
对于采用垂直切片架构的项目(请参阅 [skill:dotnet-architecture-patterns]),每个功能都在单个目录中管理自己的端点、处理程序和模型:
Features/
Products/
GetProducts.cs # Endpoint + handler + response DTO
CreateProduct.cs # Endpoint + handler + request/response DTOs
UpdateProduct.cs
DeleteProduct.cs
ProductEndpoints.cs # Route group registration
JSON 配置
Minimal API 默认使用 System.Text.Json。可以全局配置所有 Minimal API 端点的 JSON 选项:
// ConfigureHttpJsonOptions applies to Minimal APIs ONLY, not MVC controllers
builder.Services.ConfigureHttpJsonOptions(options =>
{
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
注意: ConfigureHttpJsonOptions 仅配置 Minimal API 的 JSON 序列化。MVC 控制器使用单独的管道——通过 builder.Services.AddControllers().AddJsonOptions(...) 配置。混淆两者不会产生任何效果。
参数绑定
Minimal API 会根据类型和特性注解,自动从路由、查询、标头、请求正文和 DI 中绑定参数。
// Route parameter (from URL segment)
app.MapGet("/products/{id:int}", (int id) => ...);
// Query string
app.MapGet("/products", ([FromQuery] int page, [FromQuery] int pageSize) => ...);
// Header
app.MapGet("/products", ([FromHeader(Name = "X-Correlation-Id")] string correlationId) => ...);
// Body (JSON deserialized)
app.MapPost("/products", (CreateProductDto dto) => ...);
// DI-injected services (resolved automatically)
app.MapGet("/products", (AppDbContext db, ILogger<Program> logger) => ...);
// AsParameters: bind a complex object from multiple sources
app.MapGet("/products", ([AsParameters] ProductQuery query) => ...);
public record ProductQuery(
[FromQuery] int Page = 1,
[FromQuery] int PageSize = 20,
[FromQuery] string? SortBy = null);
Agent 注意事项
- 如果可以使用
TypedResults,就不要使用Results——Results.Ok(value)返回IResult,OpenAPI 生成器无法推断响应架构。使用TypedResults.Ok(value)可启用自动架构生成。 - 不要忘记
ConfigureHttpJsonOptions仅适用于 Minimal API——MVC 控制器需要单独调用.AddControllers().AddJsonOptions()。 - 不要在每个端点中内联应用验证逻辑——请使用端点筛选器,或参阅 [skill:dotnet-input-validation],了解集中式验证模式。
- 不要以错误的顺序注册筛选器——先注册的筛选器位于最外层。先放置范围较广的筛选器(日志记录),再放置更具体的筛选器(验证),使其更接近处理程序。
- 不要将所有端点都放在
Program.cs中——端点数量超过几个后,请整理到扩展方法类或 Carter 模块中。
前提条件
- .NET 8.0+(Minimal API 使用路由组和端点筛选器的 LTS 基线版本)
- .NET 10.0,支持内置 OpenAPI 3.1、SSE 和内置验证
- 用于生成 OpenAPI 文档的
Microsoft.AspNetCore.OpenApi - 用于自动发现端点模块的
Carter(可选)
知识来源
本技能中的 Minimal API 模式以以下指南为依据:
- David Fowler——AspNetCoreDiagnosticScenarios(github.com/davidfowl/AspNetCoreDiagnosticScenarios)。这是有关 ASP.NET Core 请求管道设计、中间件最佳实践和诊断反模式的权威来源。
这些来源为上述模式和原理提供参考。本技能并不声称代表或替任何个人发言。