Skip to content
Go back

Minimal API vs Controller:ASP.NET Core 中两种 API 风格怎么选

在 ASP.NET Core 里新建一个 API 项目,第一个要面对的选择往往就是:用 Minimal API 还是 Controller?这个问题没有一刀切的答案,它跟你的团队规模、领域复杂度、以及你对代码”仪式感”的容忍度都有关系。

Controller 从 ASP.NET MVC 时代一路走到现在,积累了成熟的约定和工具链。Minimal API 则是 .NET 6 引入的轻量方案,用更函数式的语法剥掉了一堆样板代码。到了 .NET 10,Minimal API 的成熟度已经很高,功能差距大幅缩小——两种方式都不存在绝对的”谁更好”。

这篇文章把两种方式放在一起拆解,给出可对比的代码示例,帮你理清各自的优势和短板。

什么是最简 API

最简 API 让你直接在 Program.cs(或者任意你委托出去的方法)里用 MapGetMapPostMapPutMapDelete 这类扩展方法定义 HTTP 端点。处理器可以是 lambda,也可以是命名方法组,参数会自动从路由、查询字符串、请求体或服务容器中绑定。没有 Controller 类,没有基类继承,也没有属性标记那一套。

路由分组(RouteGroupBuilder)让你把相关端点组织在同一个前缀下,同时统一施加中间件或过滤器。过滤器在 Minimal API 中走的是 IEndpointFilter 接口——一个单管道式的钩子,比 Controller 完整的过滤器管道简洁,但同时也意味着功能范围更窄。

// Minimal API CRUD — .NET 10
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IProductService, ProductService>();
var app = builder.Build();

var products = app.MapGroup("/api/products").WithTags("Products");
products.MapGet("/", async (IProductService svc) =>
    Results.Ok(await svc.GetAllAsync()));
products.MapGet("/{id:int}", async (int id, IProductService svc) =>
    await svc.GetByIdAsync(id) is { } product
        ? Results.Ok(product) : Results.NotFound());
products.MapPost("/", async (CreateProductRequest request, IProductService svc) =>
{
    var created = await svc.CreateAsync(request);
    return Results.CreatedAtRoute("GetProduct", new { id = created.Id }, created);
});
products.MapPut("/{id:int}", async (int id, UpdateProductRequest request, IProductService svc) =>
{
    var updated = await svc.UpdateAsync(id, request);
    return updated is null ? Results.NotFound() : Results.Ok(updated);
});
products.MapDelete("/{id:int}", async (int id, IProductService svc) =>
{
    var deleted = await svc.DeleteAsync(id);
    return deleted ? Results.NoContent() : Results.NotFound();
});
app.Run();

代码很紧凑。处理器参数由框架自动注入——服务从 DI 容器来,路由值按名称绑定,请求体从 JSON 反序列化。没有类结构上的累赘。

Minimal API 的魅力在于清晰和速度。对于只暴露少量端点的微服务,或者偏好函数式风格的团队,这种写法天然顺手,而且不会碍事。

什么是控制器

控制器是 ASP.NET Core 的传统方式,继承自 ASP.NET MVC。你创建一个继承 ControllerBase 的类,打上 [ApiController][Route] 属性,然后定义通过 [HttpGet][HttpPost] 等方法映射到 HTTP 动词的 Action 方法。[ApiController] 属性会激活一套约定:自动模型验证、对错误输入自动返回 400、以及绑定源推断。

控制器强制了一种以类为中心的结构,天然把相关端点组织在一起。这在有几十个端点的大项目里价值很大——每个端点知道自己该放在哪里。完整的过滤器管道(IActionFilterIExceptionFilterIResourceFilterIAuthorizationFilter)让你对请求生命周期有精细的控制,这一点 Minimal API 目前无法完全复刻。

// Controller CRUD — .NET 10
[ApiController]
[Route("api/[controller]")]
public sealed class ProductsController : ControllerBase
{
    private readonly IProductService _productService;

    public ProductsController(IProductService productService)
    {
        _productService = productService;
    }

    [HttpGet]
    public async Task<IActionResult> GetAll()
        => Ok(await _productService.GetAllAsync());

    [HttpGet("{id:int}", Name = "GetProduct")]
    public async Task<IActionResult> GetById(int id)
    {
        var product = await _productService.GetByIdAsync(id);
        return product is null ? NotFound() : Ok(product);
    }

    [HttpPost]
    public async Task<IActionResult> Create(CreateProductRequest request)
    {
        var created = await _productService.CreateAsync(request);
        return CreatedAtRoute("GetProduct", new { id = created.Id }, created);
    }

    [HttpPut("{id:int}")]
    public async Task<IActionResult> Update(int id, UpdateProductRequest request)
    {
        var updated = await _productService.UpdateAsync(id, request);
        return updated is null ? NotFound() : Ok(updated);
    }

    [HttpDelete("{id:int}")]
    public async Task<IActionResult> Delete(int id)
    {
        var deleted = await _productService.DeleteAsync(id);
        return deleted ? NoContent() : NotFound();
    }
}

逻辑上几乎一模一样。Controller 版本更啰嗦,但结构很明确——一个完全没接触过这个代码库的开发者也能立刻看懂。

多维度对比

两种方式在功能上是等价的,但在几个关键维度上有明显差异:

从表上看 Controller 似乎能力更强,在某些具体领域确实如此。但对大多数 API 来说,这些额外能力根本用不到,Minimal API 的简洁反而更实在。性能差异也不应该是首要考虑因素——应该从结构和可维护性出发做判断。

依赖注入:两种都行,略有不同

依赖注入在两种方式中都是一等公民。Controller 通过构造器注入接收服务,这是标准模式:DI 容器在每个请求上构建 Controller 实例,自动注入已注册的服务。

Minimal API 主要在处理器中直接用参数注入。当逻辑迁移到专门的服务类或处理器类时,标准的构造器注入同样适用。参数注入的方式其实更显式——你直接在签名里看到某个端点依赖什么。

// Controller — 构造器注入
app.MapGet("/api/orders/{id:int}", async (
    int id,
    IOrderService orderService,
    ILogger<Program> logger) =>
{
    logger.LogInformation("Fetching order {OrderId}", id);
    var order = await orderService.GetAsync(id);
    return order is null ? Results.NotFound() : Results.Ok(order);
});

两种都能用。Minimal API 的参数注入在一个处理器需要四五个服务时会显得臃肿。这时候你可以考虑用基于类的方式,或者重构为指向服务类的方法组——保留 Minimal API 语法,但把实现从 Program.cs 搬出去。

测试体验

测试方面,Minimal API 用起来略简单一些。WebApplicationFactory<TProgram> 两种方式都适用,让你在进程内启动完整应用做集成测试。你对端点发出真实 HTTP 调用,不需要运行服务器,响应里包含完整的状态码和头信息。

Controller 还有一个额外优势:你可以写单元测试直接实例化 Controller、mock 它的依赖、然后像调用普通 C# 方法一样调用 Action。这对不经过完整 HTTP 管道、快速验证复杂逻辑的单元测试来说更快。Minimal API 的 lambda 处理器没有这个选项——你要么走 WebApplicationFactory,要么把逻辑抽到服务类里。

实际差距很小。通过 WebApplicationFactory 做集成测试是两种风格下的推荐做法,测试代码看起来完全一样,不管被测的是 Minimal API 还是 Controller。

过滤器:真正的差异点

这是 Controller 明显更强的地方。ASP.NET Core 的 Controller 过滤器管道包含五种类型:Authorization 过滤器、Resource 过滤器、Action 过滤器、Exception 过滤器和 Result 过滤器。每一种在请求生命周期的不同节点运行,可以短路管道。这给你非常精细的控制力,而且文档完备、团队普遍熟悉。

Minimal API 只有 IEndpointFilter,它在处理器执行前后提供一个钩子。你可以链多个过滤器,但它们都在管道同一个位置运行——处理器执行前和完成后。你无法用 IEndpointFilter 精确复刻 Resource 过滤器或 Exception 过滤器的语义。

对大多数 API 来说,IEndpointFilter 已经够用。日志、验证、响应塑形都能通过它完成。如果你的团队重度依赖自定义 Action 过滤器或 Exception 过滤器做横切关注点,Controller 会让你觉得更完整。

规模化组织代码

这是最诚实的权衡。只要你组织得用心,Minimal API 在中小型 API 上扩展得很漂亮。路由分组让你把相关端点聚在一起,统一施加授权策略和公用中间件。如果把端点按功能拆成扩展方法——ProductEndpoints.csOrderEndpoints.cs,每个文件注册自己的路由分组——代码会保持干净。

没有这份纪律的话,Program.cs 就会变成一堵 lambda 函数墙。这在真实项目中确实发生过。Controller 从结构上规避了这个问题:每个 Controller 文件天然有边界,类级属性作用于所有 Action,框架强制了一致的形态。

如果你在思考更大规模的组织方式,模块化单体(Modular Monolith)的思路在这里同样适用——无论你选 Minimal API 还是 Controller,真正的收益来自按功能而非技术层组织代码。

.NET 10 的 Minimal API 改进

.NET 10 延续了从 .NET 8 开始对 Minimal API 的持续投入。OpenAPI 支持通过内置的 Microsoft.AspNetCore.OpenApi 包变成一等公民(.NET 9 首次引入内置支持,.NET 10 继续增强),TypedResults 让返回类型推断可靠且准确。过滤器的文档更好,行为更一致。

通过 TypedResults.ProblemTypedResults.ValidationProblem 提供的问题详情支持,让 Minimal API 在自动 400 处理上和 Controller 的 [ApiController] 拉平。Native AOT 兼容性大幅改善,Minimal API 现在是对启动时间敏感的、高性能力求裁剪的部署场景的正当选择。

规律很清楚:每个版本都在缩小 Minimal API 和 Controller 之间的功能差距。.NET 10 已经把差距显著缩小,过去一些避免在新项目中使用 Minimal API 的理由不再成立——但团队熟悉度、项目规模和组织的既有约定仍然重要。

怎么选:一份决策矩阵

两种方式都不是普适的最优解。更好的选择往往取决于项目上下文、团队经验和优先级。

选 Minimal API 的场景:

选 Controller 的场景:

容易被忽略的一点:你可以在同一个应用里混用两种方式。一个 ASP.NET Core 应用可以同时有 Controller 端点和 Minimal API 端点,互不干扰。这让你在老项目里为新功能用 Minimal API,同时保留已有的 Controller——一种在棕地项目里逐步引入绿地功能的做法。

不止是 API 风格的选择

Minimal API 还是 Controller,本质上不是对错问题,而是适配问题。Minimal API 提供简洁、低开销和函数式风格。Controller 提供结构、更丰富的过滤器管道和大型团队依赖的约定。在 .NET 10 中,两者都足够成熟、有足够好的支持,都能构建生产级 API。

如果你今天开一个新项目,先想清楚团队规模、领域复杂度和你是更看重简洁还是显式结构。拿不准的时候,Minimal API 是专注型服务的合理默认选择,而 Controller 对复杂的、多人维护的应用来说仍然是稳妥之选。

常见问题

能在同一个项目里混用 Minimal API 和 Controller 吗?

可以,这是完全支持的配置。ASP.NET Core 路由对两种风格一视同仁,AddControllers() 和 Minimal API 端点注册可以并存,互不冲突。

这在迁移场景里尤其有用。你可以为新端点引入 Minimal API,同时不动已有的 Controller 端点。后续可以逐步把 Controller 端点迁到 Minimal API——也可以就让混合状态保持下去,如果团队用着顺手的话。

Minimal API 比 Controller 性能更好吗?

性能差异确实存在,但对多数负载来说很小。Minimal API 启动开销略低,因为它跳过了 Controller 发现过程中的反射逻辑。在 AOT 编译场景下,Minimal API 优势更明显——Controller 管道对反射的依赖在裁剪时更难处理。

对于稳态吞吐量——应用预热后的每秒请求数——大多数基准测试中差异可以忽略。两种方式都快到瓶颈几乎总在数据库或外部服务上,而不是框架路由开销。

Minimal API 支持像 [ApiController] 那样的模型验证吗?

不自动支持。Controller 的 [ApiController] 属性提供自动模型验证:如果 ModelState.IsValid 为 false,框架在 Action 运行前就返回 400。Minimal API 没有这个内置行为。

你可以通过 IEndpointFilter 和 FluentValidation 这样的验证库来复刻。.NET 10 也改进了 Minimal API 的内置验证辅助功能。虽然不如 Controller 那样全自动,但不需要太多额外代码就能实现。

怎么给 Minimal API 加日志?

加日志的方式一样——把 ILogger<T>ILogger 作为参数注入端点处理器,或者注入到端点委托的服务类中。ILogger 抽象不关心自己是在 Controller 里还是 lambda 处理器里被消费。

如果你在搭结构化日志,配置流程和用 Minimal API 还是 Controller 完全无关——日志中间件位于应用级别,不是端点级别。

应该用路由分组还是独立文件来组织 Minimal API 端点?

两个都用。路由分组提供前缀和共享配置——授权、限流、OpenAPI 标签。独立文件提供组织结构。常见模式是每个功能领域创建一个扩展方法,接收 RouteGroupBuilderWebApplication 并注册端点,然后在 Program.cs 里调用每个扩展方法。

这种做法让 Program.cs 保持整洁,每个功能有自己的文件,不增加任何框架复杂度。对中型 API 扩展良好,新成员也容易导航。

参考


Tags


Next

SQL Server 时态表 + EF Core:零侵入式数据变更审计