minimal-api
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMinimal APIs (.NET 10)
Minimal APIs(.NET 10)
Core Principles
核心原则
- Minimal APIs are the default — Use controllers only when migrating legacy code. Minimal APIs are lighter, faster, and compose well with any architecture style.
- Group endpoints with — Never scatter individual
MapGroup/MapGetcalls inMapPost. Group related endpoints together.Program.cs - Use for OpenAPI —
TypedResultsgives you compile-time type safety AND correct OpenAPI documentation.TypedResults.Ok(value)does not.Results.Ok(value) - Metadata over comments — Use ,
.WithName(),.WithTags()to document endpoints. The metadata feeds into OpenAPI specs..WithSummary()
- Minimal APIs 是默认选择 — 仅在迁移遗留代码时使用控制器。Minimal APIs 更轻量、更快速,且能与任何架构风格良好组合。
- 使用对端点进行分组 — 切勿在
MapGroup中分散调用单个Program.cs/MapGet。将相关端点组合在一起。MapPost - 为OpenAPI使用—
TypedResults可为你提供编译时类型安全以及正确的OpenAPI文档。而TypedResults.Ok(value)无法做到这一点。Results.Ok(value) - 优先使用元数据而非注释 — 使用、
.WithName()、.WithTags()来记录端点。元数据会被用于生成OpenAPI规范。.WithSummary()
Patterns
模式
Endpoint Group Auto-Discovery (Required Pattern)
端点组自动发现(必填模式)
Every endpoint group lives in its own file and implements . A single call in discovers and registers all groups automatically. Program.cs never changes when you add new endpoint groups.
IEndpointGroupapp.MapEndpoints()Program.cscsharp
// Extensions/IEndpointGroup.cs
public interface IEndpointGroup
{
void Map(IEndpointRouteBuilder app);
}csharp
// Extensions/EndpointExtensions.cs
public static class EndpointExtensions
{
public static WebApplication MapEndpoints(this WebApplication app)
{
var groups = typeof(Program).Assembly
.GetTypes()
.Where(t => t.IsAssignableTo(typeof(IEndpointGroup)) && !t.IsInterface && !t.IsAbstract)
.Select(Activator.CreateInstance)
.Cast<IEndpointGroup>();
foreach (var group in groups)
group.Map(app);
return app;
}
}csharp
// Program.cs — this NEVER changes when adding endpoints
var app = builder.Build();
app.MapEndpoints();
app.Run();csharp
// Features/Orders/OrderEndpoints.cs — one file per endpoint group
public sealed class OrderEndpoints : IEndpointGroup
{
public void Map(IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/orders").WithTags("Orders");
group.MapPost("/", CreateOrder)
.WithName("CreateOrder")
.WithSummary("Create a new order")
.Produces<OrderResponse>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.RequireAuthorization();
group.MapGet("/{id:guid}", GetOrder)
.WithName("GetOrder")
.Produces<OrderResponse>()
.ProducesProblem(StatusCodes.Status404NotFound);
group.MapGet("/", ListOrders)
.WithName("ListOrders")
.Produces<PagedList<OrderResponse>>();
}
private static async Task<Results<Created<OrderResponse>, ValidationProblem>> CreateOrder(
CreateOrderRequest request,
ISender sender,
CancellationToken ct)
{
var result = await sender.Send(new CreateOrder.Command(request.CustomerId, request.Items), ct);
return result.IsSuccess
? TypedResults.Created($"/api/orders/{result.Value.Id}", result.Value)
: TypedResults.ValidationProblem(result.Errors);
}
private static async Task<Results<Ok<OrderResponse>, NotFound>> GetOrder(
Guid id,
ISender sender,
CancellationToken ct)
{
var result = await sender.Send(new GetOrder.Query(id), ct);
return result.IsSuccess
? TypedResults.Ok(result.Value)
: TypedResults.NotFound();
}
private static async Task<Ok<PagedList<OrderResponse>>> ListOrders(
[AsParameters] ListOrdersQuery query,
ISender sender,
CancellationToken ct)
{
var result = await sender.Send(query, ct);
return TypedResults.Ok(result);
}
}每个端点组都位于独立文件中,并实现接口。在中调用一次即可自动发现并注册所有组。添加新端点组时,无需任何修改。
IEndpointGroupProgram.csapp.MapEndpoints()Program.cscsharp
// Extensions/IEndpointGroup.cs
public interface IEndpointGroup
{
void Map(IEndpointRouteBuilder app);
}csharp
// Extensions/EndpointExtensions.cs
public static class EndpointExtensions
{
public static WebApplication MapEndpoints(this WebApplication app)
{
var groups = typeof(Program).Assembly
.GetTypes()
.Where(t => t.IsAssignableTo(typeof(IEndpointGroup)) && !t.IsInterface && !t.IsAbstract)
.Select(Activator.CreateInstance)
.Cast<IEndpointGroup>();
foreach (var group in groups)
group.Map(app);
return app;
}
}csharp
// Program.cs — 添加端点时永远无需修改此处
var app = builder.Build();
app.MapEndpoints();
app.Run();csharp
// Features/Orders/OrderEndpoints.cs — 每个端点组对应一个文件
public sealed class OrderEndpoints : IEndpointGroup
{
public void Map(IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/orders").WithTags("Orders");
group.MapPost("/", CreateOrder)
.WithName("CreateOrder")
.WithSummary("Create a new order")
.Produces<OrderResponse>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.RequireAuthorization();
group.MapGet("/{id:guid}", GetOrder)
.WithName("GetOrder")
.Produces<OrderResponse>()
.ProducesProblem(StatusCodes.Status404NotFound);
group.MapGet("/", ListOrders)
.WithName("ListOrders")
.Produces<PagedList<OrderResponse>>();
}
private static async Task<Results<Created<OrderResponse>, ValidationProblem>> CreateOrder(
CreateOrderRequest request,
ISender sender,
CancellationToken ct)
{
var result = await sender.Send(new CreateOrder.Command(request.CustomerId, request.Items), ct);
return result.IsSuccess
? TypedResults.Created($"/api/orders/{result.Value.Id}", result.Value)
: TypedResults.ValidationProblem(result.Errors);
}
private static async Task<Results<Ok<OrderResponse>, NotFound>> GetOrder(
Guid id,
ISender sender,
CancellationToken ct)
{
var result = await sender.Send(new GetOrder.Query(id), ct);
return result.IsSuccess
? TypedResults.Ok(result.Value)
: TypedResults.NotFound();
}
private static async Task<Ok<PagedList<OrderResponse>>> ListOrders(
[AsParameters] ListOrdersQuery query,
ISender sender,
CancellationToken ct)
{
var result = await sender.Send(query, ct);
return TypedResults.Ok(result);
}
}TypedResults for Type-Safe Responses
使用TypedResults实现类型安全响应
TypedResultscsharp
// GOOD — TypedResults with union return type
private static async Task<Results<Ok<Product>, NotFound, ValidationProblem>> GetProduct(
Guid id,
AppDbContext db,
CancellationToken ct)
{
var product = await db.Products.FindAsync([id], ct);
return product is not null
? TypedResults.Ok(product)
: TypedResults.NotFound();
}TypedResultscsharp
// 推荐用法 — 带联合返回类型的TypedResults
private static async Task<Results<Ok<Product>, NotFound, ValidationProblem>> GetProduct(
Guid id,
AppDbContext db,
CancellationToken ct)
{
var product = await db.Products.FindAsync([id], ct);
return product is not null
? TypedResults.Ok(product)
: TypedResults.NotFound();
}Parameter Binding
参数绑定
.NET 10 minimal APIs bind parameters from route, query, header, body, and DI automatically.
csharp
// Route parameters
app.MapGet("/orders/{id:guid}", (Guid id) => ...);
// Query parameters (nullable = optional)
app.MapGet("/orders", (int page, int? pageSize, string? status) => ...);
// Complex query parameters with [AsParameters]
public record ListOrdersQuery(int Page = 1, int PageSize = 20, string? Status = null);
app.MapGet("/orders", ([AsParameters] ListOrdersQuery query) => ...);
// Header binding
app.MapGet("/orders", ([FromHeader(Name = "X-Correlation-Id")] string? correlationId) => ...);
// DI services are auto-resolved (no attribute needed)
app.MapPost("/orders", (CreateOrderRequest request, ISender sender) => ...);.NET 10 Minimal APIs会自动从路由、查询、请求头、请求体和依赖注入中绑定参数。
csharp
// 路由参数
app.MapGet("/orders/{id:guid}", (Guid id) => ...);
// 查询参数(可空表示可选)
app.MapGet("/orders", (int page, int? pageSize, string? status) => ...);
// 使用[AsParameters]绑定复杂查询参数
public record ListOrdersQuery(int Page = 1, int PageSize = 20, string? Status = null);
app.MapGet("/orders", ([AsParameters] ListOrdersQuery query) => ...);
// 请求头绑定
app.MapGet("/orders", ([FromHeader(Name = "X-Correlation-Id")] string? correlationId) => ...);
// DI服务会自动解析(无需属性)
app.MapPost("/orders", (CreateOrderRequest request, ISender sender) => ...);Endpoint Filters
端点过滤器
Filters are the minimal API equivalent of action filters. Use them for cross-cutting concerns like validation, logging, and idempotency checks.
The canonical implementation (FluentValidation, resolves the validator from DI and skips gracefully when none is registered) lives in the error-handling skill — use that one, don't re-implement it per project.
ValidationFilter<TRequest>csharp
// Apply the canonical filter (see error-handling skill) to a mutating endpoint
group.MapPost("/", CreateOrder)
.AddEndpointFilter<ValidationFilter<CreateOrderRequest>>();
// Apply a filter to a group (affects all endpoints in the group)
group.AddEndpointFilter<LoggingFilter>();过滤器是Minimal API中与动作过滤器等效的组件。可用于处理验证、日志、幂等性检查等横切关注点。
标准的实现(基于FluentValidation,从DI中解析验证器,当未注册时优雅跳过)位于错误处理技能中 — 直接使用该实现,不要在每个项目中重复编写。
ValidationFilter<TRequest>csharp
// 为可变端点应用标准过滤器(请查看错误处理技能)
group.MapPost("/", CreateOrder)
.AddEndpointFilter<ValidationFilter<CreateOrderRequest>>();
// 为组应用过滤器(影响组内所有端点)
group.AddEndpointFilter<LoggingFilter>();OpenAPI / Swagger Configuration
OpenAPI / Swagger配置
.NET 10 has built-in OpenAPI support. Use it instead of Swashbuckle.
csharp
// Program.cs — service registration only, no endpoint wiring
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.MapEndpoints(); // auto-discovers all IEndpointGroup implementations
// Endpoint metadata enriches the OpenAPI spec
group.MapPost("/", CreateOrder)
.WithName("CreateOrder")
.WithSummary("Create a new order")
.WithDescription("Creates a new order for the specified customer with the given line items.")
.Produces<OrderResponse>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.ProducesProblem(StatusCodes.Status500InternalServerError);.NET 10内置OpenAPI支持,请使用内置功能而非Swashbuckle。
csharp
// Program.cs — 仅注册服务,无需配置端点
builder.Services.AddOpenApi();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.MapEndpoints(); // 自动发现所有IEndpointGroup实现
// 端点元数据会丰富OpenAPI规范
group.MapPost("/", CreateOrder)
.WithName("CreateOrder")
.WithSummary("Create a new order")
.WithDescription("Creates a new order for the specified customer with the given line items.")
.Produces<OrderResponse>(StatusCodes.Status201Created)
.ProducesValidationProblem()
.ProducesProblem(StatusCodes.Status500InternalServerError);Rate Limiting
速率限制
csharp
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("api", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromMinutes(1);
});
});
// Apply inside an IEndpointGroup.Map method
var group = app.MapGroup("/api/orders")
.WithTags("Orders")
.RequireRateLimiting("api");csharp
builder.Services.AddRateLimiter(options =>
{
options.AddFixedWindowLimiter("api", opt =>
{
opt.PermitLimit = 100;
opt.Window = TimeSpan.FromMinutes(1);
});
});
// 在IEndpointGroup.Map方法内应用
var group = app.MapGroup("/api/orders")
.WithTags("Orders")
.RequireRateLimiting("api");Output Caching
输出缓存
csharp
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder => builder.Expire(TimeSpan.FromMinutes(5)));
options.AddPolicy("ByIdCache", builder => builder
.Expire(TimeSpan.FromMinutes(10))
.SetVaryByRouteValue("id"));
});
group.MapGet("/{id:guid}", GetOrder)
.CacheOutput("ByIdCache");csharp
builder.Services.AddOutputCache(options =>
{
options.AddBasePolicy(builder => builder.Expire(TimeSpan.FromMinutes(5)));
options.AddPolicy("ByIdCache", builder => builder
.Expire(TimeSpan.FromMinutes(10))
.SetVaryByRouteValue("id"));
});
group.MapGet("/{id:guid}", GetOrder)
.CacheOutput("ByIdCache");Anti-patterns
反模式
Don't Put Endpoints in Program.cs
不要将端点放在Program.cs中
csharp
// BAD — endpoints scattered in Program.cs
app.MapGet("/orders", async (AppDbContext db) => await db.Orders.ToListAsync());
app.MapGet("/orders/{id}", async (Guid id, AppDbContext db) => await db.Orders.FindAsync(id));
app.MapPost("/orders", async (Order order, AppDbContext db) => { /* ... */ });
app.MapGet("/products", async (AppDbContext db) => await db.Products.ToListAsync());
// ALSO BAD — manual MapGroup calls in Program.cs (grows with every feature)
app.MapGroup("/api/orders").WithTags("Orders").MapOrderEndpoints();
app.MapGroup("/api/products").WithTags("Products").MapProductEndpoints();
app.MapGroup("/api/customers").WithTags("Customers").MapCustomerEndpoints();
// Program.cs grows every time you add a feature...
// GOOD — auto-discovered, Program.cs never changes
app.MapEndpoints(); // discovers all IEndpointGroup implementationscsharp
// 错误示例 — 端点分散在Program.cs中
app.MapGet("/orders", async (AppDbContext db) => await db.Orders.ToListAsync());
app.MapGet("/orders/{id}", async (Guid id, AppDbContext db) => await db.Orders.FindAsync(id));
app.MapPost("/orders", async (Order order, AppDbContext db) => { /* ... */ });
app.MapGet("/products", async (AppDbContext db) => await db.Products.ToListAsync());
// 同样错误 — 在Program.cs中手动调用MapGroup(会随功能增加而膨胀)
app.MapGroup("/api/orders").WithTags("Orders").MapOrderEndpoints();
app.MapGroup("/api/products").WithTags("Products").MapProductEndpoints();
app.MapGroup("/api/customers").WithTags("Customers").MapCustomerEndpoints();
// 每次添加功能,Program.cs都会变大...
// 正确示例 — 自动发现,Program.cs永远无需修改
app.MapEndpoints(); // 发现所有IEndpointGroup实现Don't Use Untyped Results
不要使用无类型结果
csharp
// BAD — Results.Ok doesn't contribute to OpenAPI schema
private static async Task<IResult> GetOrder(Guid id, AppDbContext db)
{
var order = await db.Orders.FindAsync(id);
return order is not null ? Results.Ok(order) : Results.NotFound();
}
// GOOD — TypedResults with explicit union type
private static async Task<Results<Ok<Order>, NotFound>> GetOrder(Guid id, AppDbContext db)
{
var order = await db.Orders.FindAsync(id);
return order is not null ? TypedResults.Ok(order) : TypedResults.NotFound();
}csharp
// 错误示例 — Results.Ok不会生成OpenAPI架构
private static async Task<IResult> GetOrder(Guid id, AppDbContext db)
{
var order = await db.Orders.FindAsync(id);
return order is not null ? Results.Ok(order) : Results.NotFound();
}
// 正确示例 — 带显式联合类型的TypedResults
private static async Task<Results<Ok<Order>, NotFound>> GetOrder(Guid id, AppDbContext db)
{
var order = await db.Orders.FindAsync(id);
return order is not null ? TypedResults.Ok(order) : TypedResults.NotFound();
}Don't Return Domain Entities Directly
不要直接返回领域实体
csharp
// BAD — leaks internal structure, can't evolve independently
app.MapGet("/orders/{id}", async (Guid id, AppDbContext db) =>
await db.Orders.Include(o => o.Items).FirstOrDefaultAsync(o => o.Id == id));
// GOOD — map to a response DTO
app.MapGet("/orders/{id}", async (Guid id, AppDbContext db) =>
{
var order = await db.Orders
.Where(o => o.Id == id)
.Select(o => new OrderResponse(o.Id, o.Total, o.CreatedAt))
.FirstOrDefaultAsync();
return order is not null ? TypedResults.Ok(order) : TypedResults.NotFound();
});csharp
// 错误示例 — 暴露内部结构,无法独立演进
app.MapGet("/orders/{id}", async (Guid id, AppDbContext db) =>
await db.Orders.Include(o => o.Items).FirstOrDefaultAsync(o => o.Id == id));
// 正确示例 — 映射为响应DTO
app.MapGet("/orders/{id}", async (Guid id, AppDbContext db) =>
{
var order = await db.Orders
.Where(o => o.Id == id)
.Select(o => new OrderResponse(o.Id, o.Total, o.CreatedAt))
.FirstOrDefaultAsync();
return order is not null ? TypedResults.Ok(order) : TypedResults.NotFound();
});Decision Guide
决策指南
| Scenario | Recommendation |
|---|---|
| New HTTP API | |
| Existing MVC project | Keep controllers, migrate incrementally |
| OpenAPI documentation | Use |
| Request validation | Endpoint filter with FluentValidation |
| Authentication/authorization | |
| Rate limiting | |
| Response caching | |
| Complex model binding | |
| 场景 | 推荐方案 |
|---|---|
| 新建HTTP API | 每个功能对应一个 |
| 现有MVC项目 | 保留控制器,逐步迁移 |
| OpenAPI文档 | 使用 |
| 请求验证 | 结合FluentValidation的端点过滤器 |
| 认证/授权 | 在组或端点上使用 |
| 速率限制 | |
| 响应缓存 | |
| 复杂模型绑定 | 结合记录类型使用 |