一、什么是 Minimal API?
ASP.NET Core 从 .NET 6 开始引入了 Minimal API,它是一种极简风格的 HTTP API 构建方式,旨在以最少的代码、最少的依赖、最少的仪式感来定义 Web 端点。
与传统的 MVC Controller 模式相比,Minimal API 不需要定义 Controller 类、不需要继承任何基类、不需要标注大量 Attribute------一切都围绕 路由 + 委托(Delegate) 展开,极度贴近 HTTP 本质。
csharp
// 这就是一个完整可运行的 Minimal API 应用
var app = WebApplication.Create(args);
app.MapGet("/hello", () => "Hello, World!");
app.Run();
二、核心设计理念
2.1 约定优于配置,但更少约定
MVC 的设计哲学是"约定优于配置(Convention over Configuration)"------你遵循命名规范,框架自动帮你做很多事。但这背后隐藏了大量隐式约定,增加了学习曲线。
Minimal API 的理念更进一步:显式优于隐式。路由、参数、返回值------所有东西都写在一起,一眼就能看明白。
2.2 函数式端点 vs 面向对象端点
| 维度 | MVC Controller | Minimal API |
|---|---|---|
| 结构单元 | Class(控制器) | Delegate(委托/函数) |
| 路由定义 | Attribute 标注 | MapGet/Post/... 链式调用 |
| 依赖注入 | 构造函数注入 | 参数自动绑定 |
| 启动仪式 | 需要 AddControllers + MapControllers |
开箱即用 |
| 文件量 | 多文件、多类 | 单文件可完成 |
2.3 统一的请求管道
Minimal API 并不是另起炉灶,它完全基于 ASP.NET Core 现有的中间件管道(Middleware Pipeline),只是将"最终处理器(Terminal Handler)"从 Controller Action 换成了一个普通的委托。这意味着你熟悉的一切------身份认证、授权、日志、CORS、限流------全部可以原封不动地用。
三、基础功能全览
3.1 路由映射
Minimal API 提供了对应 HTTP 动词的一系列 Map* 扩展方法:
csharp
app.MapGet("/items", () => { /* ... */ });
app.MapPost("/items", () => { /* ... */ });
app.MapPut("/items/{id}", (int id) => { /* ... */ });
app.MapDelete("/items/{id}", (int id) => { /* ... */ });
app.MapPatch("/items/{id}", (int id) => { /* ... */ });
// 通配任意方法
app.MapMethods("/items", new[] { "GET", "HEAD" }, () => { /* ... */ });
路由模板支持参数、约束、可选段,与 MVC 完全一致:
csharp
// 路由参数约束
app.MapGet("/users/{id:int:min(1)}", (int id) => $"User {id}");
// 通配符路由
app.MapGet("/files/{**path}", (string path) => $"File: {path}");
3.2 参数绑定(Parameter Binding)
这是 Minimal API 最强大的特性之一。框架会根据参数的来源 和类型自动推断绑定方式,无需手动标注(大多数情况下):
csharp
app.MapGet("/search", (
string keyword, // 自动绑定 Query String: ?keyword=...
int page = 1, // 带默认值,可选
int pageSize = 20
) => { /* ... */ });
app.MapGet("/users/{id}", (
int id, // 路由参数
HttpContext context // 框架内置对象,直接注入
) => { /* ... */ });
app.MapPost("/items", (
CreateItemRequest body, // 自动从 JSON Body 反序列化
IItemService service // 从 DI 容器注入
) => { /* ... */ });
绑定优先级(从高到低):
[FromRoute] → [FromQuery] → [FromHeader] → [FromBody] → [FromServices] → 默认推断
推断规则:
- 复杂类型 → 默认尝试
[FromBody](JSON) - 简单类型(string/int/bool/...)→ 先路由,再 Query String
- 已注册的 DI 服务 → 自动从容器获取(.NET 7+)
- 特殊类型 (
HttpContext、HttpRequest、CancellationToken等)→ 框架直接提供
自定义绑定(TryParse / BindAsync)
csharp
// 实现 TryParse 即可让简单类型支持路由/Query绑定
public record Point(int X, int Y)
{
public static bool TryParse(string value, out Point? point)
{
var parts = value.Split(',');
if (parts.Length == 2 && int.TryParse(parts[0], out int x) && int.TryParse(parts[1], out int y))
{
point = new Point(x, y);
return true;
}
point = null;
return false;
}
}
app.MapGet("/point/{p}", (Point p) => p); // GET /point/3,4 → { X: 3, Y: 4 }
csharp
// 实现 BindAsync 支持从 HttpContext 自定义绑定复杂类型
public class PaginationOptions
{
public int Page { get; set; } = 1;
public int PageSize { get; set; } = 20;
public static ValueTask<PaginationOptions?> BindAsync(HttpContext context, ParameterInfo parameter)
{
int.TryParse(context.Request.Query["page"], out int page);
int.TryParse(context.Request.Query["pageSize"], out int pageSize);
return ValueTask.FromResult<PaginationOptions?>(new PaginationOptions
{
Page = page > 0 ? page : 1,
PageSize = pageSize > 0 ? pageSize : 20
});
}
}
3.3 返回值处理
Minimal API 的 Handler 可以返回任意类型,框架会智能处理:
csharp
// 返回字符串 → text/plain
app.MapGet("/str", () => "Hello");
// 返回 C# 对象 → 自动序列化为 JSON
app.MapGet("/obj", () => new { Name = "Alice", Age = 30 });
// 返回 IResult → 精确控制响应
app.MapGet("/result", () => Results.Ok(new { Status = "ok" }));
app.MapGet("/notfound", () => Results.NotFound("Resource not found"));
app.MapGet("/redirect", () => Results.Redirect("https://example.com"));
// 返回 Task/ValueTask → 异步支持
app.MapGet("/async", async (IDbService db) =>
{
var items = await db.GetAllAsync();
return Results.Ok(items);
});
TypedResults(.NET 7+)
TypedResults 是 Results 的强类型版本,为 OpenAPI 文档生成提供更准确的元数据:
csharp
app.MapGet("/users/{id}", async (int id, IUserService svc) =>
{
var user = await svc.GetByIdAsync(id);
return user is null
? TypedResults.NotFound()
: TypedResults.Ok(user);
})
.Produces<User>(200)
.Produces(404);
3.4 路由分组(Route Groups)
.NET 7 引入了 MapGroup,解决了代码组织和公共前缀/过滤器的问题:
csharp
var api = app.MapGroup("/api/v1");
var users = api.MapGroup("/users").RequireAuthorization();
users.MapGet("/", GetAllUsers);
users.MapGet("/{id}", GetUserById);
users.MapPost("/", CreateUser);
users.MapDelete("/{id}", DeleteUser);
// 嵌套分组
var admin = api.MapGroup("/admin")
.RequireAuthorization("AdminPolicy")
.AddEndpointFilter<AuditFilter>();
3.5 过滤器(Endpoint Filters)
类似 MVC 的 ActionFilter,Minimal API 的过滤器通过 IEndpointFilter 接口实现:
csharp
// 定义过滤器
public class ValidationFilter<T> : IEndpointFilter where T : class
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var model = context.Arguments.OfType<T>().FirstOrDefault();
if (model is null)
return Results.BadRequest("Invalid request body");
var validator = context.HttpContext.RequestServices
.GetRequiredService<IValidator<T>>();
var result = await validator.ValidateAsync(model);
if (!result.IsValid)
return Results.ValidationProblem(result.ToDictionary());
return await next(context); // 调用下一个过滤器或实际 Handler
}
}
// 使用过滤器
app.MapPost("/items", CreateItem)
.AddEndpointFilter<ValidationFilter<CreateItemRequest>>();
四、执行流程与内部原理
这是理解 Minimal API 的关键。表面上看,MapGet 注册一个 Lambda 就完成了端点定义,但背后经历了一系列精密的编译时和运行时处理。
4.1 整体架构
HTTP 请求到达
↓
Kestrel(HTTP 服务器)
↓
中间件管道(Middleware Pipeline)
├─ 异常处理
├─ HTTPS 重定向
├─ 身份认证
├─ 授权
└─ ...
↓
EndpointRoutingMiddleware(路由匹配)
↓
EndpointMiddleware(执行端点)
↓
RequestDelegate(Minimal API 生成的委托)
↓
参数绑定 → Handler 执行 → 响应写入
4.2 启动阶段:端点注册
当你调用 app.MapGet("/hello", () => "Hello") 时,内部发生了什么?
第一步:创建 RouteEndpointBuilder
csharp
// 简化版源码逻辑
public static RouteHandlerBuilder MapGet(
this IEndpointRouteBuilder endpoints,
string pattern,
Delegate handler)
{
return endpoints.MapMethods(pattern, new[] { "GET" }, handler);
}
第二步:RequestDelegateFactory 分析并编译 Handler
这是核心所在。RequestDelegateFactory 通过反射分析 (或 .NET 7+ 的源生成器 )检查 Handler 的参数和返回类型,然后生成 一个 RequestDelegate(即 Func<HttpContext, Task>)。
csharp
// 你写的代码
app.MapGet("/users/{id}", (int id, IUserService svc) => svc.GetById(id));
// 框架在内部生成类似这样的 RequestDelegate:
RequestDelegate generated = async (HttpContext httpContext) =>
{
// 1. 绑定路由参数
if (!int.TryParse(httpContext.GetRouteValue("id")?.ToString(), out int id))
{
httpContext.Response.StatusCode = 400;
return;
}
// 2. 从 DI 获取服务
var svc = httpContext.RequestServices.GetRequiredService<IUserService>();
// 3. 执行 Handler
var result = svc.GetById(id);
// 4. 序列化响应
await httpContext.Response.WriteAsJsonAsync(result);
};
这个"生成"过程在**应用启动时(Build 阶段)**完成,而不是每次请求时。这意味着反射的成本只在启动时发生一次,运行时性能接近手写代码。
第三步:构建 Endpoint 并注册到路由表
生成的 RequestDelegate 被包装进一个 RouteEndpoint 对象,其中包含路由模板、HTTP 方法约束、元数据(用于 OpenAPI、授权等)等信息,最终注册到 EndpointDataSource。
4.3 请求阶段:路由与执行
EndpointRoutingMiddleware 负责路由匹配:
- 解析请求路径,与路由表中所有
RouteEndpoint进行匹配 - 使用
RouteMatcher(基于 Trie 树的高效算法)找到最佳匹配 - 将匹配到的
Endpoint存入HttpContext.Features中 - 将路由参数存入
RouteValueDictionary
EndpointMiddleware 负责执行:
csharp
// 极简版 EndpointMiddleware 实现
public async Task InvokeAsync(HttpContext context)
{
var endpoint = context.GetEndpoint();
if (endpoint?.RequestDelegate is not null)
{
// 执行过滤器链 + 实际 Handler
await endpoint.RequestDelegate(context);
}
else
{
await _next(context);
}
}
4.4 参数绑定的详细流程
RequestDelegate 被调用
↓
遍历 Handler 的每个参数
↓
├─ 是 HttpContext / HttpRequest / HttpResponse / CancellationToken?
│ → 直接从框架提供
│
├─ 标注了 [FromRoute]?
│ → 从 RouteValues 取值 → TryParse 转换类型
│
├─ 标注了 [FromQuery]?
│ → 从 QueryString 取值 → TryParse 转换类型
│
├─ 标注了 [FromHeader]?
│ → 从 Headers 取值 → TryParse 转换类型
│
├─ 标注了 [FromBody]?
│ → 读取 Body → 反序列化(System.Text.Json)
│
├─ 标注了 [FromServices]?
│ → RequestServices.GetRequiredService<T>()
│
├─ 未标注,推断:
│ ├─ 类型已注册到 DI? → 当作服务注入(.NET 7+)
│ ├─ 复杂类型? → 尝试 BindAsync → 尝试 [FromBody]
│ └─ 简单类型? → 路由参数 → Query String
│
└─ 参数为 nullable 或有默认值? → 允许缺失,否则返回 400
4.5 过滤器管道的执行原理
过滤器构成一个责任链(Chain of Responsibility),与中间件管道同构:
csharp
// 框架内部构建过滤器链的简化逻辑
EndpointFilterDelegate pipeline = async (ctx) =>
{
// 最内层:执行实际 Handler
var result = handler(ctx.Arguments[0], ctx.Arguments[1], ...);
return await ResultExtensions.ExecuteAsync(result, ctx.HttpContext);
};
// 逆序包装每个过滤器(最后注册的最先执行外层)
foreach (var filter in filters.Reverse())
{
var next = pipeline;
pipeline = async (ctx) => await filter.InvokeAsync(ctx, next);
}
五、与依赖注入(DI)的深度集成
5.1 服务注册
csharp
var builder = WebApplication.CreateBuilder(args);
// 注册服务
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddSingleton<ICacheService, RedisCacheService>();
builder.Services.AddDbContext<AppDbContext>(opt =>
opt.UseSqlServer(builder.Configuration.GetConnectionString("Default")));
var app = builder.Build();
5.2 Handler 中消费服务
csharp
// 方式一:参数注入(推荐,.NET 7+ 自动识别已注册服务)
app.MapGet("/users", async (IUserRepository repo, ICacheService cache) =>
{
var cached = await cache.GetAsync<List<User>>("users");
if (cached is not null) return Results.Ok(cached);
var users = await repo.GetAllAsync();
await cache.SetAsync("users", users, TimeSpan.FromMinutes(5));
return Results.Ok(users);
});
// 方式二:显式标注(更明确)
app.MapGet("/users", async ([FromServices] IUserRepository repo) => { /* ... */ });
// 方式三:从 HttpContext 手动获取(不推荐,破坏可测试性)
app.MapGet("/users", async (HttpContext ctx) =>
{
var repo = ctx.RequestServices.GetRequiredService<IUserRepository>();
});
六、常见使用场景
场景一:轻量级微服务
Minimal API 非常适合只需要暴露少量 HTTP 端点的微服务。相比 MVC,它的启动更快、内存占用更小、代码量更少:
csharp
// 一个完整的"用户微服务",单文件即可
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddDbContext<UserDbContext>(...);
var app = builder.Build();
app.MapGet("/users", async (IUserRepository repo) =>
Results.Ok(await repo.GetAllAsync()));
app.MapGet("/users/{id}", async (int id, IUserRepository repo) =>
await repo.GetByIdAsync(id) is { } user
? Results.Ok(user)
: Results.NotFound());
app.MapPost("/users", async (CreateUserDto dto, IUserRepository repo) =>
{
var user = await repo.CreateAsync(dto);
return Results.Created($"/users/{user.Id}", user);
});
app.Run();
场景二:BFF(Backend for Frontend)
BFF 层通常需要聚合多个下游服务,Minimal API 的简洁性让这类转发/聚合代码更加清晰:
csharp
app.MapGet("/dashboard", async (
IOrderService orders,
IUserService users,
INotificationService notifications,
ClaimsPrincipal principal) =>
{
var userId = int.Parse(principal.FindFirst("sub")!.Value);
var (userTask, ordersTask, notifTask) = (
users.GetProfileAsync(userId),
orders.GetRecentAsync(userId),
notifications.GetUnreadCountAsync(userId)
);
await Task.WhenAll(userTask, ordersTask, notifTask);
return Results.Ok(new
{
User = await userTask,
RecentOrders = await ordersTask,
UnreadNotifications = await notifTask
});
});
场景三:原型快速开发
产品早期需要快速验证接口契约,Minimal API 可以让后端同学在几分钟内搭出可用的接口:
csharp
// 用内存字典模拟数据,快速验证前端联调
var items = new Dictionary<int, string> { [1] = "Item A", [2] = "Item B" };
var nextId = 3;
app.MapGet("/items", () => items);
app.MapGet("/items/{id}", (int id) =>
items.TryGetValue(id, out var item) ? Results.Ok(item) : Results.NotFound());
app.MapPost("/items", (string name) => { items[nextId++] = name; return Results.Created(); });
场景四:Webhook 接收端
csharp
app.MapPost("/webhooks/github", async (
HttpRequest request,
[FromHeader(Name = "X-Hub-Signature-256")] string signature,
IGithubWebhookHandler handler) =>
{
using var reader = new StreamReader(request.Body);
var payload = await reader.ReadToEndAsync();
if (!handler.VerifySignature(payload, signature))
return Results.Unauthorized();
var @event = JsonSerializer.Deserialize<GithubEvent>(payload);
await handler.ProcessAsync(@event!);
return Results.Accepted();
});
七、代码组织最佳实践
随着端点增多,把所有路由塞在 Program.cs 会变得难以维护。以下是几种组织方式:
7.1 静态扩展方法(推荐)
csharp
// UserEndpoints.cs
public static class UserEndpoints
{
public static IEndpointRouteBuilder MapUserEndpoints(
this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/users").RequireAuthorization();
group.MapGet("/", GetAll);
group.MapGet("/{id}", GetById);
group.MapPost("/", Create);
group.MapPut("/{id}", Update);
group.MapDelete("/{id}", Delete);
return app;
}
private static async Task<IResult> GetAll(IUserService svc)
=> Results.Ok(await svc.GetAllAsync());
private static async Task<IResult> GetById(int id, IUserService svc)
=> await svc.GetByIdAsync(id) is { } user
? Results.Ok(user)
: Results.NotFound();
// ...
}
// Program.cs
app.MapUserEndpoints();
app.MapOrderEndpoints();
app.MapProductEndpoints();
7.2 IEndpointDefinition 接口模式
csharp
// 定义接口
public interface IEndpointDefinition
{
void DefineEndpoints(WebApplication app);
void DefineServices(IServiceCollection services) { }
}
// 实现
public class UserEndpointDefinition : IEndpointDefinition
{
public void DefineServices(IServiceCollection services)
=> services.AddScoped<IUserService, UserService>();
public void DefineEndpoints(WebApplication app)
{
var group = app.MapGroup("/api/users");
group.MapGet("/", GetAll);
// ...
}
}
// 自动扫描注册(反射实现)
public static class WebApplicationExtensions
{
public static void UseEndpointDefinitions(this WebApplication app)
{
var definitions = typeof(Program).Assembly
.GetTypes()
.Where(t => t.IsAssignableTo(typeof(IEndpointDefinition)) && !t.IsAbstract)
.Select(Activator.CreateInstance)
.Cast<IEndpointDefinition>();
foreach (var def in definitions)
def.DefineEndpoints(app);
}
}
八、OpenAPI / Swagger 集成
.NET 9 中 Minimal API 的 OpenAPI 支持已经大幅成熟:
csharp
builder.Services.AddOpenApi();
app.MapOpenApi(); // 生成 /openapi/v1.json
app.MapScalarApiReference(); // Scalar UI(替代 SwaggerUI)
// 为端点添加元数据
app.MapGet("/users/{id}", GetUser)
.WithName("GetUser")
.WithSummary("获取用户详情")
.WithDescription("根据用户 ID 返回用户的完整信息")
.WithTags("Users")
.Produces<UserResponse>(200)
.Produces<ProblemDetails>(404)
.RequireAuthorization();
九、性能对比与优势
Minimal API 在性能上相比 MVC 有一定优势,根据 TechEmpower Benchmark:
- 启动时间:更少的程序集扫描和反射,启动速度提升明显(尤其配合 Native AOT)
- 内存占用:更小的基础占用(无 Controller 激活、无 ActionDescriptor 等开销)
- 请求吞吐量 :得益于
RequestDelegateFactory的预编译,运行时几乎无反射开销
Native AOT 支持(.NET 8+)
csharp
// 配合源生成器,实现无反射的参数绑定和 JSON 序列化
[JsonSerializable(typeof(User))]
[JsonSerializable(typeof(List<User>))]
public partial class AppJsonContext : JsonSerializerContext { }
builder.Services.ConfigureHttpJsonOptions(opt =>
opt.SerializerOptions.TypeInfoResolverChain.Add(AppJsonContext.Default));
十、Minimal API vs MVC:如何选择?
| 维度 | 选 Minimal API | 选 MVC |
|---|---|---|
| 团队规模 | 小团队、全栈 | 大团队、明确分层 |
| 项目规模 | 微服务、少量端点 | 单体应用、大量端点 |
| 学习曲线 | 更低(新人友好) | 较高(约定多) |
| 代码组织 | 需要自行设计 | 框架强制分层 |
| 测试方式 | 单元测试友好(纯函数) | 需要 Mock 更多框架对象 |
| 视图渲染 | 不支持(纯 API) | 支持 Razor、Blazor |
| 生态成熟度 | 较新,持续演进 | 成熟稳定 |
结论 :对于新项目,如果是纯 API 服务(无视图),推荐默认选择 Minimal API;如果项目规模很大、团队需要明确的 MVC 分层规范,或者需要集成 Razor Views,则选 MVC。两者也可以混用------在同一个项目中同时注册 Controller 和 Minimal API 端点。
十一、总结
Minimal API 不是一个"简化版"的 API 框架,而是 ASP.NET Core 对 HTTP 编程模型的一次重新思考 。它把视角从"类与方法的映射"拉回到"路由与处理函数"的本质,让开发者能够以最直接的方式表达"当 GET /users/{id} 到来时,执行这段逻辑"。
其核心价值在于:
- 启动极简,几行代码即可运行
- 参数绑定智能,减少大量样板代码
- 性能出色,预编译委托、Native AOT 友好
- 完全兼容中间件、DI、身份认证等核心基础设施
- 可组合性强 ,通过
MapGroup+ 扩展方法可以优雅地组织大型项目
随着 .NET 每个大版本的迭代(.NET 6 → 7 → 8 → 9),Minimal API 的功能在持续增强,已经成为现代 .NET Web 开发的重要范式,值得每一位 .NET 开发者深入掌握。