Web API 与 Minimal API

系统讲解 ASP.NET Core Web API 与 Minimal API 的开发方式,覆盖 RESTful 设计原则、Minimal API 路由与依赖注入、OpenAPI 文档生成、模型绑定与验证,以及 API 版本控制的落地实践。

1. RESTful API 设计原则

一句话总结: REST 以资源为中心,用 HTTP 方法表达操作语义、用状态码表达结果,良好的资源命名与超媒体链接是 API 可演进性的基础。

RESTful 设计把一切抽象为资源(Resource),每个资源有唯一的 URL,操作通过 HTTP 方法表达:GET 读、POST 建、PUT 全量更新、PATCH 局部更新、DELETE 删。URL 只含名词复数,动词交给方法。

// 反例:把动词放进 URL
GET  /api/getOrders
POST /api/createOrder
GET  /api/deleteOrder/5

// 正例:资源 + 方法
GET    /api/orders        // 列表
POST   /api/orders        // 新建
GET    /api/orders/{id}   // 单个
PUT    /api/orders/{id}   // 全量更新
PATCH  /api/orders/{id}   // 局部更新
DELETE /api/orders/{id}   // 删除
原则要点
资源命名复数名词、小写、连字符分隔
方法语义GET 幂等只读、PUT 幂等、POST 非幂等
状态码200/201/204/400/404/409 各司其职
无状态服务端不保存客户端会话状态
可发现性集合端点返回子资源链接

避坑: 不要把查询参数复杂化到「伪 RPC」。筛选、排序、分页用查询字符串(?status=paid&page=2),但仍然保持资源语义,而不是 ?action=computeDiscount。状态码宁可用 409 表达业务冲突,也不要一律返回 200 + 错误码字段。

2. Minimal API 与传统 Controller

一句话总结: Minimal API 用最少的样板暴露 HTTP 端点,适合小型服务与演示项目;Controller 模式适合团队大型应用,两者在同一应用中可以共存。

Minimal API 把「路由 + 处理逻辑」压缩成一行 Lambda,没有 Controller 类、没有 [ApiController]、没有基类继承,依赖注入与配置仍完整保留。它非常适合微服务、健康检查、BFF 等场景。

// Program.cs —— Minimal API 最小示例
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello World");

app.MapGet("/orders/{id}", async (IOrderService svc, int id) =>
{
    var order = await svc.GetByIdAsync(id);
    return order is null ? Results.NotFound() : Results.Ok(order);
});

app.MapPost("/orders", async (IOrderService svc, CreateOrderRequest req) =>
{
    var id = await svc.CreateAsync(req);
    return Results.Created($"/orders/{id}", id);
});

app.Run();
// 传统 Controller 等价实现
[ApiController]
[Route("api/orders")]
public class OrdersController(IOrderService svc) : ControllerBase
{
    [HttpGet("{id:int}")]
    public async Task<IActionResult> GetById(int id)
    {
        var order = await svc.GetByIdAsync(id);
        return order is null ? NotFound() : Ok(order);
    }

    [HttpPost]
    public async Task<IActionResult> Create(CreateOrderRequest req)
    {
        var id = await svc.CreateAsync(req);
        return Created($"/api/orders/{id}", id);
    }
}
维度Minimal APIController
样板量极低较高
路由过滤MapXxx + 通配符属性路由 + 约束
自动模型验证需手动调用[ApiController] 自动
适合场景小服务、演示、工具端点大型领域 API

一句话: 选择标准不是「哪个更酷」,而是团队的认知负载。几十个端点的小服务用 Minimal API 清爽直接;几十个实体的领域 API 用 Controller 更便于组织验证、过滤器与约定。

3. Minimal API 路由与参数绑定

一句话总结: Minimal API 的路由支持约束与通配符,参数通过委托签名自动绑定,lambda 参数的类型决定了来源是路径、查询还是请求体。

Minimal API 的参数绑定规则:{id:int} 路径段绑定到同名的简单类型参数;复杂类型默认从 JSON 请求体反序列化;[FromQuery]、[FromHeader] 等特性可显式指定来源。路由约束让非法输入提前 404。

// 路由约束:int 只匹配整数,不符合返回 404
app.MapGet("/orders/{id:int}", (int id) => ...);
app.MapGet("/orders/{code:length(4,16)}", (string code) => ...);

// 查询与 Header 显式来源
app.MapGet("/orders", (
    [FromQuery] int page = 1,
    [FromQuery] int size = 20,
    [FromHeader(Name = "X-Request-Id")] string? requestId) =>
{
    return Results.Ok(new { page, size, requestId });
});

// 请求体复杂类型
app.MapPost("/orders", (CreateOrderRequest req) => ...);

// 文件上传
app.MapPost("/upload", async (IFormFile file) =>
{
    await using var fs = File.Create(Path.Combine("uploads", file.FileName));
    await file.CopyToAsync(fs);
    return Results.Ok(new { name = file.FileName, size = file.Length });
});
绑定来源写法
路径参数{id:int} → int id
查询参数同名简单类型参数
请求体复杂类型自动 JSON 反序列化
Header[FromHeader] 特性
文件IFormFile 类型

避坑: 简单类型参数默认不是从请求体取,而是从路径/查询取。想让 int 从 body 来必须用 [FromBody]。另外 Minimal API 不自动做模型验证(Controller 的 [ApiController] 会),参数校验要自己写,或者显式调用 Results.ValidationProblem。

4. OpenAPI 与 Swagger 文档生成

一句话总结: 通过 AddOpenApi 自动生成 OpenAPI 文档,Swagger UI 提供交互式调试界面,按需用特性或 XML 注释增强文档信息。

ASP.NET Core 9+ 内置 Microsoft.AspNetCore.OpenApi,一行代码即可生成 OpenAPI 3 文档;Swagger UI 让前端与测试可以直接在浏览器里试请求。Minimal API 的端点也能通过 WithName、WithSummary、Produces 等扩展方法补充元数据。

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOpenApi();                    // 生成 /openapi/v1.json

var app = builder.Build();

app.MapOpenApi();                                  // 暴露 JSON 端点
app.MapSwagger();                                  // 可选:Swagger UI 页面

// 为端点补充 OpenAPI 元数据
app.MapGet("/orders/{id:int}", async (IOrderService svc, int id) =>
    await svc.GetByIdAsync(id) is { } o ? Results.Ok(o) : Results.NotFound())
   .WithName("GetOrder")
   .WithSummary("按 ID 获取订单")
   .WithDescription("返回订单详情,不存在时返回 404")
   .Produces<Order>(StatusCodes.Status200OK)
   .Produces(StatusCodes.Status404NotFound);

app.Run();
// 生成的 openapi/v1.json 片段
{
  "openapi": "3.0.1",
  "paths": {
    "/orders/{id}": {
      "get": {
        "operationId": "GetOrder",
        "summary": "按 ID 获取订单",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": {
          "200": { "description": "Success" },
          "404": { "description": "Not Found" }
        }
      }
    }
  }
}
工具作用
AddOpenApi()注册 OpenAPI 文档生成
MapOpenApi()暴露 /openapi/v1.json
MapSwagger()Swagger UI 交互页
WithSummary/WithName端点文档元数据
Produces<T>声明响应类型与状态码

避坑: 生产环境不要把 Swagger UI 暴露到公网——它是攻击面的信息源。正确做法是仅开发/测试环境启用,生产关闭或加鉴权。文档生成依赖返回类型推断,lambda 里显式 Results.Ok<T>(...) 能让类型更准确。

5. 模型绑定与 Data Annotation 验证

一句话总结: 模型绑定把请求数据映射到参数对象,Data Annotation 提供声明式校验规则,Controller 模式自动触发而 Minimal API 需要手动调用 Validate。

模型绑定从请求的路径、查询、Header、Body 组装出目标对象。校验则靠 [Required]、[Range]、[StringLength]、[EmailAddress] 等特性描述规则。Controller 的 [ApiController] 会在绑定后自动验证并返回 400。

// 请求模型:声明式校验规则
public class CreateOrderRequest
{
    [Required]
    [StringLength(64, MinimumLength = 2)]
    public string CustomerName { get; set; } = string.Empty;

    [Range(0.01, 1_000_000)]
    public decimal Total { get; set; }

    [Required]
    public List<OrderLineDto> Lines { get; set; } = [];
}

public class OrderLineDto
{
    [Required]
    public int ProductId { get; set; }

    [Range(1, 100)]
    public int Quantity { get; set; }
}
// Minimal API:手动校验(Controller 自动)
app.MapPost("/orders", async (IOrderService svc, CreateOrderRequest req) =>
{
    // 需要引入 Microsoft.AspNetCore.Http.HttpResults 扩展的 Validate 方法
    // 或自行校验后返回 ValidationProblem
    if (!Validator.TryValidateObject(req, new ValidationContext(req), out var errors))
    {
        return Results.ValidationProblem(
            errors.GroupBy(e => e.MemberNames.FirstOrDefault() ?? "")
                  .ToDictionary(g => g.Key, g => g.Select(e => e.ErrorMessage ?? "").ToArray()));
    }

    var id = await svc.CreateAsync(req);
    return Results.Created($"/orders/{id}", id);
});
校验手段说明
[Required] / [Range]内置 Data Annotation
[StringLength]长度约束
IValidatableObject跨字段自定义规则
FluentValidation链式规则,更强大
Validator.TryValidateObject手动触发校验

避坑: Controller 的自动验证返回 400 前不会进入方法体,而 Minimal API 全靠自觉——漏了 Validate 就漏了校验,脏数据直达业务层。复杂跨字段规则(如「折扣价不能高于原价」)建议用 IValidatableObject 或 FluentValidation。

6. API 版本控制

一句话总结: API 演进必须考虑兼容性,URL 路径版本是最直观的方案,配合响应与文档的版本隔离,让新旧客户端并行使用。

版本控制解决「老客户端还在用、新接口又要上」的兼容问题。ASP.NET Core 通过 AddApiVersioning 支持 URL 路径、查询串、Header 三种策略。路径版本(/api/v2/orders)最直观,也是兼容成本最低的默认选择。

// 引入 Asp.Versioning.Http 包
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;      // 响应头返回 api-supported-versions
}).AddApiExplorer();                        // 让 OpenAPI 显示各版本

var app = builder.Build();

var v1 = app.MapGroup("/api/v1/orders");
v1.MapGet("/", async (IOrderService svc) => Results.Ok(await svc.ListV1Async()));
v1.MapGet("/{id:int}", async (IOrderService svc, int id) => ...);

var v2 = app.MapGroup("/api/v2/orders");
v2.MapGet("/", async (IOrderService svc) => Results.Ok(await svc.ListV2Async()));
版本策略载体优点缺点
URL 路径/api/v2/orders直观、缓存友好URL 长期暴露旧版
查询串?api-version=2URL 干净容易被遗忘
HeaderX-Version: 2路径不变不直观
Media TypeAccept: vnd.api.v2+jsonRESTful 纯正实现复杂

避坑: 版本不是「加个 v2 就完事」。每个版本都要有独立的生命周期与下线计划,废弃版本应提前声明并返回 Deprecation 头。改动兼容(加字段、加可选参数)完全不需要新版本,只有破坏性变更才升版本。

7. 错误处理与统一返回

一句话总结: 统一的问题详情(RFC 7807)让错误可机器消费,全局异常中间件把未处理异常收敛成结构化响应,避免堆栈泄漏。

ASP.NET Core 内置 ProblemDetails 标准(RFC 7807),Results.Problem 与 IExceptionHandler 让错误响应结构一致。异常处理中间件兜底所有未捕获异常,开发环境返回详情、生产环境隐藏内部错误。

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddProblemDetails();          // 注册 ProblemDetails 服务
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

var app = builder.Build();
app.UseExceptionHandler();                      // 启用全局异常中间件

app.MapGet("/orders/{id:int}", async (IOrderService svc, int id) =>
{
    var order = await svc.GetByIdAsync(id);
    return order is null
        ? Results.Problem(
            statusCode: 404,
            title: "订单不存在",
            detail: $"找不到 ID 为 {id} 的订单")
        : Results.Ok(order);
});
// 全局异常处理:收敛未捕获异常
public class GlobalExceptionHandler : IExceptionHandler
{
    public ValueTask<bool> TryHandleAsync(
        HttpContext ctx, Exception ex, CancellationToken ct)
    {
        ctx.Response.StatusCode = StatusCodes.Status500InternalServerError;
        ctx.Response.ContentType = "application/problem+json";
        return ctx.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Status = 500,
            Title = "服务内部错误",
            Detail = ctx.RequestServices.GetService<IHostEnvironment>()?
                             .IsDevelopment() == true ? ex.ToString() : null
        }, ct).ContinueWith(_ => new ValueTask<bool>(true)).GetAwaiter().GetResult() is var r
            ? r : new ValueTask<bool>(true);
    }
}
场景响应
参数校验失败400 ValidationProblemDetails
资源不存在404 ProblemDetails
业务冲突409 ProblemDetails
未处理异常500 ProblemDetails(隐藏堆栈)

避坑: 别把 Exception 的堆栈直接写进生产响应——那是信息泄漏。生产环境只给稳定 ID(如 traceId),把完整堆栈交给日志系统。ProblemDetails 的 type 字段应指向一个可读的文档 URL,而不是空字符串。

8. 总结

环节要点
REST 设计资源 + 方法 + 状态码,动词不进 URL
Minimal vs Controller小服务 Minimal,大领域 Controller,可共存
路由绑定路径约束 + 委托参数自动绑定
OpenAPIAddOpenApi 生成文档,Swagger UI 调试
验证Data Annotation 声明规则,Minimal 需手动触发
版本控制URL 路径版本最直观,兼容变更不升版本
错误处理ProblemDetails 统一结构 + 全局异常兜底

Web API 的价值在于用 HTTP 语义表达业务,让客户端与工具都能理解。REST 的克制(资源化、方法化、状态码化)与 Minimal API 的简洁并不冲突——前者是设计原则,后者是实现手段。把契约(OpenAPI)、验证(Data Annotation)、错误(ProblemDetails)这三样固定下来,API 的可演进性与可协作性就有了地基。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「csharp」更多文章

  1. 消息与后台任务
  2. 缓存与并发控制
  3. 测试体系:xUnit 与 Moq