Minimal API 深入
大约 12 分钟约 3601 字
Minimal API 深入
简介
Minimal API 是 ASP.NET Core 6+ 引入的轻量级 API 开发方式,通过 Lambda 表达式和顶级语句(Top-level Statements)直接在 Program.cs 中定义 HTTP 端点,无需 Controller 类、路由模板和视图引擎等 MVC 管道开销。在 .NET 8 中,Minimal API 已经非常成熟,支持参数绑定、端点过滤器、路由分组、OpenAPI 集成、结果类型、依赖注入和请求验证等完整功能。深入理解 Minimal API 的参数绑定机制、过滤器管道、TypedResults 与 Results 的区别、路由分组策略和与 Controller 的取舍,有助于在微服务、内部 API 和轻量项目中做出正确的架构选择。
特点
参数绑定深入
绑定源优先级
var app = builder.Build();
// 参数绑定源优先级(从高到低):
// 1. 显式标记:[FromBody], [FromRoute], [FromQuery], [FromHeader], [FromServices]
// 2. 路由参数:匹配路由模板中的 {param}
// 3. 查询参数:匹配 URL 查询字符串
// 4. 请求体:POST/PUT 的 JSON Body
// === 路由参数绑定 ===
app.MapGet("/users/{id:int}", (int id) =>
{
// id 从路由 /users/{id} 绑定
return TypedResults.Ok(new { UserId = id });
});
// 多个路由参数
app.MapGet("/users/{userId:int}/orders/{orderId:int}", (int userId, int orderId) =>
{
return TypedResults.Ok(new { UserId = userId, OrderId = orderId });
});
// === 查询参数绑定 ===
app.MapGet("/search", (string? keyword, int page = 1, int pageSize = 20, string? sortBy = null) =>
{
// keyword, page, pageSize, sortBy 从查询字符串绑定
// GET /search?keyword=hello&page=2&pageSize=10&sortBy=name
return TypedResults.Ok(new { keyword, page, pageSize, sortBy });
});
// === 请求体绑定 ===
app.MapPost("/users", (CreateUserRequest request) =>
{
// request 从 JSON Body 反序列化
return TypedResults.Created($"/users/{request.Id}", request);
});
// === Header 绑定 ===
app.MapGet("/profile", ([FromHeader(Name = "X-Request-Id")] string requestId) =>
{
return TypedResults.Ok(new { RequestId = requestId });
});
// === 服务注入绑定 ===
app.MapGet("/users/{id:int}", async (int id, IUserService userService) =>
{
var user = await userService.GetByIdAsync(id);
return user is not null ? TypedResults.Ok(user) : TypedResults.NotFound();
});
// === HttpContext 绑定 ===
app.MapPost("/upload", async (HttpContext ctx) =>
{
var form = await ctx.Request.ReadFormAsync();
var file = form.Files["file"];
return TypedResults.Ok(new { file?.FileName, file?.Length });
}).DisableAntiforgery();
// === 组合绑定 ===
app.MapPost("/orders", (
[FromBody] CreateOrderRequest body,
[FromHeader(Name = "X-Request-Id")] string requestId,
[FromServices] IOrderService orderService,
ClaimsPrincipal user) =>
{
var userId = user.FindFirst("sub")?.Value;
// 创建订单...
return TypedResults.Ok(new { orderId = Guid.NewGuid(), requestId });
});
public record CreateUserRequest(string Name, string Email);
public record CreateOrderRequest(List<OrderItemDto> Items, string ShippingAddress);
public record OrderItemDto(string ProductName, int Quantity, decimal UnitPrice);自定义参数绑定
// 实现 TryParse 方法实现自定义绑定
public readonly struct PaginationParams
{
public int Page { get; }
public int PageSize { get; }
public PaginationParams(int page, int pageSize)
{
Page = Math.Max(1, page);
PageSize = Math.Clamp(pageSize, 1, 100);
}
public static bool TryParse(string? value, out PaginationParams result)
{
// 从查询字符串解析: "page=1&pageSize=20"
// Minimal API 会自动调用 TryParse
result = new PaginationParams(1, 20);
return true;
}
}
// 使用
app.MapGet("/api/products", (PaginationParams pagination) =>
{
return TypedResults.Ok(new { pagination.Page, pagination.PageSize });
});
// 使用 BindAsync 实现复杂绑定
public class SearchCriteria
{
public string? Keyword { get; set; }
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
public string? SortBy { get; set; }
public SortDirection SortDirection { get; set; } = SortDirection.Ascending;
public decimal? MinPrice { get; set; }
public decimal? MaxPrice { get; set; }
public static async ValueTask<SearchCriteria?> BindAsync(
HttpContext context,
ParameterInfo parameter)
{
var query = context.Request.Query;
var criteria = new SearchCriteria
{
Keyword = query["keyword"],
Page = int.TryParse(query["page"], out var page) ? page : 1,
PageSize = int.TryParse(query["pageSize"], out var size) ? size : 20,
SortBy = query["sortBy"],
MinPrice = decimal.TryParse(query["minPrice"], out var min) ? min : null,
MaxPrice = decimal.TryParse(query["maxPrice"], out var max) ? max : null
};
if (Enum.TryParse<SortDirection>(query["sortDir"], true, out var dir))
criteria.SortDirection = dir;
return criteria;
}
}TypedResults vs Results
区别与选择
// === Results(动态类型) ===
// - 返回类型是 IResult
// - OpenAPI 无法推断响应类型
// - 灵活但缺少类型安全
app.MapGet("/users/dynamic/{id}", (int id) =>
{
if (id <= 0) return Results.BadRequest("无效的 ID");
return Results.Ok(new { Id = id, Name = "张三" });
});
// Swagger 显示: 200 (object), 400 (object) — 无法知道具体类型
// === TypedResults(静态类型) ===
// - 返回类型是具体的 TypedResults.Ok<T>
// - OpenAPI 可以推断响应类型
// - 编译时类型检查
app.MapGet("/users/typed/{id}", (int id) =>
{
if (id <= 0) return TypedResults.BadRequest("无效的 ID");
return TypedResults.Ok(new UserDto(id, "张三"));
});
// Swagger 显示: 200 (UserDto), 400 (string) — 精确类型
// TypedResults 常用方法
TypedResults.Ok(data) // 200 + JSON body
TypedResults.Created(uri, data) // 201 + Location + JSON body
TypedResults.NoContent() // 204
TypedResults.NotFound() // 404
TypedResults.BadRequest(error) // 400
TypedResults.Unauthorized() // 401
TypedResults.Forbidden() // 403
TypedResults.Conflict(error) // 409
TypedResults.ValidationProblem(details) // 400 (RFC 7807)
TypedResults.Problem(details, statusCode: 500) // 自定义状态码 (RFC 7807)
TypedResults.Stream(stream, contentType) // 文件流
TypedResults.File(bytes, contentType, fileName) // 文件下载
// 推荐做法:
// 1. 单个端点返回相同类型 → 使用 TypedResults(类型安全 + OpenAPI 文档)
// 2. 端点返回多种类型 → 使用 Results.Accepted/Results.Ok 等组合
// 3. 需要精确的 OpenAPI 文档 → 优先 TypedResults端点过滤器
IEndpointFilter 基础
// 端点过滤器在路由匹配后、端点处理器之前执行
// 类似 Controller 的 Action Filter,但更轻量
// 验证过滤器
app.MapPost("/users", (CreateUserRequest req) => TypedResults.Created("/users/1", req))
.AddEndpointFilter(async (context, next) =>
{
var request = context.GetArgument<CreateUserRequest>(0);
// 验证逻辑
var errors = new List<string>();
if (string.IsNullOrWhiteSpace(request.Name))
errors.Add("姓名不能为空");
if (string.IsNullOrWhiteSpace(request.Email))
errors.Add("邮箱不能为空");
if (request.Email?.Contains("@") != true)
errors.Add("邮箱格式不正确");
if (errors.Count > 0)
{
return TypedResults.ValidationProblem(
errors.ToDictionary(e => "Error", e => e.Select(_ => e).ToArray()));
}
return await next(context);
});
// 限流过滤器
app.MapGet("/api/data", () => "data")
.AddEndpointFilter(async (context, next) =>
{
var clientId = context.HttpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
// 限流逻辑...
return await next(context);
});
// 日志过滤器 — 记录请求耗时
public class TimingEndpointFilter : IEndpointFilter
{
private readonly ILogger<TimingEndpointFilter> _logger;
public TimingEndpointFilter(ILogger<TimingEndpointFilter> logger)
{
_logger = logger;
}
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var sw = Stopwatch.StartNew();
var result = await next(context);
sw.Stop();
_logger.LogInformation(
"{Method} {Path} → {StatusCode} ({ElapsedMs}ms)",
context.HttpContext.Request.Method,
context.HttpContext.Request.Path,
context.HttpContext.Response.StatusCode,
sw.ElapsedMilliseconds);
return result;
}
}
// 使用
app.MapGet("/api/products", () => "products")
.AddEndpointFilter<TimingEndpointFilter>();过滤器管道与短路
// 多个过滤器按添加顺序执行
// 前置处理(next 之前)按添加顺序执行
// 后置处理(next 之后)按添加顺序的逆序执行
app.MapPost("/orders", (CreateOrderRequest req) => TypedResults.Ok(req))
.AddEndpointFilter(async (context, next) =>
{
Console.WriteLine("过滤器 1: 前置");
var result = await next(context);
Console.WriteLine("过滤器 1: 后置");
return result;
})
.AddEndpointFilter(async (context, next) =>
{
Console.WriteLine("过滤器 2: 前置");
var result = await next(context);
Console.WriteLine("过滤器 2: 后置");
return result;
});
// 执行顺序:
// 过滤器 1: 前置
// 过滤器 2: 前置
// 端点处理器
// 过滤器 2: 后置
// 过滤器 1: 后置
// 短路过滤器 — 不调用 next,直接返回结果
app.MapGet("/admin/data", () => "admin data")
.AddEndpointFilter(async (context, next) =>
{
if (!context.HttpContext.User.IsInRole("Admin"))
{
return TypedResults.Forbid(); // 短路:不执行后续过滤器和端点
}
return await next(context);
});
// 全局过滤器 — 对所有端点生效
builder.Services.AddEndpointsFilterFactory((endpoint, filterFactories) =>
{
// 所有端点添加日志过滤器
filterFactories.Add(new TimingEndpointFilter());
});路由分组
基础分组
// 路由组批量添加公共前缀、中间件和元数据
var api = app.MapGroup("/api")
.WithOpenApi(); // 组级别 OpenAPI 元数据
// 认证组
var auth = api.MapGroup("/auth");
auth.MapPost("/login", (LoginRequest req, IAuthService authService) =>
TypedResults.Ok(authService.Login(req.Username, req.Password)));
auth.MapPost("/register", (RegisterRequest req, IAuthService authService) =>
TypedResults.Created("/api/auth/profile", authService.Register(req)));
auth.MapPost("/refresh", (RefreshTokenRequest req, IAuthService authService) =>
TypedResults.Ok(authService.RefreshToken(req.RefreshToken)));
// 用户组 — 需要认证
var users = api.MapGroup("/users")
.RequireAuthorization()
.WithTags("用户管理");
users.MapGet("/", (IUserService userService) =>
TypedResults.Ok(userService.GetAll()))
.WithSummary("获取用户列表");
users.MapGet("/{id:int}", async (int id, IUserService userService) =>
{
var user = await userService.GetByIdAsync(id);
return user is not null ? TypedResults.Ok(user) : TypedResults.NotFound();
})
.WithSummary("获取用户详情");
users.MapPost("/", (CreateUserRequest req, IUserService userService) =>
TypedResults.Created($"/api/users/{Guid.NewGuid()}", userService.Create(req)))
.WithSummary("创建用户")
.AddEndpointFilter<CreateUserValidationFilter>();
// 管理员组 — 需要管理员角色
var admin = api.MapGroup("/admin")
.RequireAuthorization("AdminPolicy")
.WithTags("管理后台");
admin.MapGet("/dashboard", () => TypedResults.Ok(new { message = "管理员仪表盘" }));
admin.MapGet("/users", (IUserService userService) => TypedResults.Ok(userService.GetAll()));
admin.MapGet("/system/logs", (ILogService logService) => TypedResults.Ok(logService.GetRecentLogs()));API 版本分组
// API 版本管理
var v1 = app.MapGroup("/api/v1")
.WithOpenApi()
.WithTags("v1");
var v2 = app.MapGroup("/api/v2")
.WithOpenApi()
.WithTags("v2");
// v1 端点
v1.MapGet("/products", (IProductService service) =>
TypedResults.Ok(service.GetAllV1()))
.WithSummary("获取商品列表 (v1)");
// v2 端点 — 增加了分页和过滤
v2.MapGet("/products", (
[AsParameters] ProductQueryParams queryParams,
IProductService service) =>
{
var result = service.GetAllV2(queryParams.Page, queryParams.PageSize, queryParams.Category);
return TypedResults.Ok(result);
})
.WithSummary("获取商品列表 (v2) - 支持分页和过滤");
public record ProductQueryParams(
int Page = 1,
int PageSize = 20,
string? Category = null);路由组与中间件
// 路由组可以添加组级别的中间件和过滤器
var publicApi = app.MapGroup("/api/public")
.RequireRateLimiting("PublicRateLimit")
.CacheOutput("PublicCache")
.WithTags("公共 API");
publicApi.MapGet("/health", () => TypedResults.Ok(new { status = "healthy" }));
publicApi.MapGet("/config", (IAppConfigService config) => TypedResults.Ok(config.GetPublicConfig()));
// 带限流和缓存的组
var dataApi = app.MapGroup("/api/data")
.RequireAuthorization()
.RequireRateLimiting("DataRateLimit")
.AddEndpointFilter<RequestLoggingFilter>()
.WithTags("数据 API");
dataApi.MapGet("/products", (IProductService service) => TypedResults.Ok(service.GetAll()));
dataApi.MapGet("/categories", (ICategoryService service) => TypedResults.Ok(service.GetAll()));OpenAPI 集成
Swagger 配置
// 注册 Swagger 服务
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "My API",
Version = "v1",
Description = "ASP.NET Core Minimal API"
});
// JWT 认证
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT Authorization header: Bearer {token}",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }
},
Array.Empty<string>()
}
});
});
app.UseSwagger();
app.UseSwaggerUI();
// 端点级别的 OpenAPI 元数据
app.MapGet("/users/{id:int}", async (int id, IUserService service) =>
{
var user = await service.GetByIdAsync(id);
return user is not null ? TypedResults.Ok(user) : TypedResults.NotFound();
})
.WithName("GetUser")
.WithSummary("获取用户详情")
.WithDescription("根据用户 ID 获取用户详细信息")
.WithTags("用户管理")
.Produces<UserDto>(StatusCodes.Status200OK)
.Produces(StatusCodes.Status404NotFound);实际项目组织
大型项目的端点组织
// Program.cs — 保持简洁
var builder = WebApplication.CreateBuilder(args);
// 配置服务
builder.Services.AddDbContext<AppDbContext>(/* ... */);
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IOrderService, OrderService>();
// 配置 Swagger
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// 配置认证授权
builder.Services.AddAuthentication(/* ... */);
builder.Services.AddAuthorization();
var app = builder.Build();
// 配置中间件
app.UseSwagger();
app.UseSwaggerUI();
app.UseAuthentication();
app.UseAuthorization();
// 注册端点(分文件组织)
app.MapAuthEndpoints();
app.MapUserEndpoints();
app.MapOrderEndpoints();
app.MapProductEndpoints();
app.Run();
// === Extensions/AuthEndpoints.cs ===
public static class AuthEndpoints
{
public static void MapAuthEndpoints(this WebApplication app)
{
var group = app.MapGroup("/api/auth")
.WithTags("认证");
group.MapPost("/login", async (
LoginRequest request,
IAuthService authService) =>
{
var result = await authService.LoginAsync(request);
return result is not null
? TypedResults.Ok(result)
: TypedResults.Unauthorized();
})
.WithSummary("用户登录")
.Produces<LoginResponse>(200)
.Produces(401);
group.MapPost("/register", async (
RegisterRequest request,
IAuthService authService) =>
{
var userId = await authService.RegisterAsync(request);
return TypedResults.Created($"/api/users/{userId}", new { userId });
})
.WithSummary("用户注册")
.Produces(201)
.Produces<ValidationProblemDetails>(400);
}
}
// === Extensions/UserEndpoints.cs ===
public static class UserEndpoints
{
public static void MapUserEndpoints(this WebApplication app)
{
var group = app.MapGroup("/api/users")
.RequireAuthorization()
.WithTags("用户管理");
group.MapGet("/", async (IUserService service) =>
TypedResults.Ok(await service.GetAllAsync()))
.WithSummary("获取用户列表");
group.MapGet("/{id:int}", async (int id, IUserService service) =>
{
var user = await service.GetByIdAsync(id);
return user is not null ? TypedResults.Ok(user) : TypedResults.NotFound();
})
.WithSummary("获取用户详情")
.Produces<UserDto>(200)
.Produces(404);
}
}性能考量
Minimal API vs Controller 性能
Minimal API 的性能优势:
- 更少的反射开销(Lambda 直接编译为委托)
- 更少的中间件管道(跳过 MVC Action Invoker)
- 更少的内存分配(无需 Controller 实例)
基准测试参考(.NET 8):
Minimal API: ~500,000 req/s (简单 JSON 响应)
Controller API: ~400,000 req/s (同等逻辑)
差异约 20-25%
但在实际项目中,瓶颈通常是:
- 数据库查询
- 外部 API 调用
- 序列化/反序列化
- 网络延迟
这些远大于框架本身的差异。
因此:选择 Minimal API 的理由应该是代码简洁性和开发效率,
而非仅仅为了性能。优点
缺点
总结
Minimal API 适合微服务、内部 API 和轻量 HTTP 端点场景。路由参数自动绑定(优先级:显式标记 > 路由参数 > 查询参数 > 请求体),复杂绑定通过 TryParse 或 BindAsync 实现。TypedResults 提供编译时类型安全和精确的 OpenAPI 文档,推荐优先使用。IEndpointFilter 实现验证、日志、限流等横切关注点,支持管道链和短路。RouteGroup 批量添加前缀、中间件、授权和 OpenAPI 元数据。大型项目建议将端点注册拆分到扩展方法文件中(app.MapXxxEndpoints())。纯 API 项目优先 Minimal API,需要视图引擎的 Web 应用继续使用 Controller。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
- 框架能力的真正重点是它在请求链路中的位置和对上下游的影响。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
- 画清执行顺序、入参来源、失败返回和日志记录点。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
- 知道 API 名称,却不知道它应该放在请求链路的哪个位置。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
- 继续补齐协议选型、网关治理、端点可观测性和契约演进策略。
适用场景
- 当你准备把《Minimal API 深入》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《Minimal API 深入》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《Minimal API 深入》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《Minimal API 深入》最大的收益和代价分别是什么?
