Skip to content
Go back

ASP.NET Core Web API 完全指南:请求管道、认证、错误处理到生产部署

入口节点、认证检查点、中心错误枢纽、测试框架四节点流动封面

用 .NET 构建 HTTP 服务这件事,从来没有现在这么顺手。ASP.NET Core Web API 是 Microsoft 提供的 RESTful 服务框架,浏览器、移动端、桌面客户端、其他后端服务都是它的消费方。它站在开发体验和原始性能的交叉点上——而到了 .NET 10,速度和能力又上了一台阶。

这篇指南覆盖从请求管道到生产部署的完整链路:中间件如何串联、路由如何工作、JWT 认证和策略授权怎么配、错误怎样统一成 ProblemDetails 格式、模型验证如何免手写、集成测试如何用 WebApplicationFactory 测完整管道,以及 Docker 和 Azure 的部署选项。

为什么选 ASP.NET Core

ASP.NET Core 是跨平台的——Windows、Linux、macOS,代码不用改一行。它内置 Kestrel,一个异步高性能 HTTP 服务器,在 TechEmpower 框架基准测试中常年名列前茅。内置的依赖注入容器开箱即用,不需要第三方 IoC 库。

.NET 10 继续加码。Native AOT 对 Web API 项目的支持改善明显,适合按秒计费的容器化工作负载(兼容性还取决于项目使用的特性和库)。Minimal API 已经成熟到可以作为微服务端点的可行选择。内置的 Microsoft.AspNetCore.OpenApi 包消除了很多项目里的第三方 OpenAPI 依赖。

框架本身会处理那些繁琐的 HTTP 细节:模型验证、内容协商、RFC 9457 ProblemDetails 错误格式、基于特性的路由、完整的中间件基础设施——全都内置。你写业务逻辑;ASP.NET Core 处理 HTTP 的仪式。

请求管道:理解它是基础

请求管道是 ASP.NET Core 的基础,理解它之后,后面所有问题都会更容易解释。每个 HTTP 请求从 Kestrel 进入,依次穿过一组中间件组件,才能到达端点处理器。每个中间件都可以检查请求、修改响应、短路整条链,或者把控制权传给下一个组件。

一个典型的 .NET 10 Web API Program.cs

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApi();

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = builder.Configuration["Auth:Authority"];
        options.Audience = builder.Configuration["Auth:Audience"];
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ClockSkew = TimeSpan.FromSeconds(30)
        };
    });

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("AdminOnly", policy => policy.RequireRole("Admin"));
});

builder.Services.AddScoped<IProductService, ProductService>();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

var app = builder.Build();

app.MapOpenApi();

// 异常处理必须放最前面,才能捕获完整管道里的异常
app.UseExceptionHandler("/error");
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

中间件顺序至关重要UseAuthentication 必须放在 UseAuthorization 之前,两者都必须在路由之后——而 MapControllers 在 .NET 10 里已经隐式处理了路由注册。顺序错了,安全策略会在每个请求上静默失效,没有任何报错。

管道本质上是一个链表。每个中间件包裹下一个。当你在中间件里调用 await next(context) 时,控制权交给下一个组件。这种可组合性让日志、缓存、限流、HTTPS 重定向等横切关注点可以在不触碰端点逻辑的前提下注入进去。

路由:URL 到处理器的映射

路由把 HTTP 请求的 URL 和动词映射到具体的端点处理器,同时提取路径参数。ASP.NET Core 有两种路由风格:传统路由(主要用于 MVC 渲染 HTML)和属性路由(Web API 的标准)。

Web API 通常优先用属性路由,因为路由定义直接写在控制器和 Action 上,API 接口一目了然:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult GetAll() => Ok(new[] { "Widget", "Gadget" });

    [HttpGet("{id:int}")]
    public IActionResult GetById(int id) => Ok(new { Id = id, Name = "Widget" });

    [HttpPost]
    public IActionResult Create([FromBody] CreateProductRequest request)
    {
        return CreatedAtAction(nameof(GetById), new { id = 42 }, request);
    }

    [HttpDelete("{id:int}")]
    public IActionResult Delete(int id) => NoContent();
}

public record CreateProductRequest(string Name, decimal Price);

路由模板里的 [controller] 标记在运行时会被控制器名减去 Controller 后缀替换。{id:int} 这类路由约束把参数限制为整数,不满足时直接返回 404。框架内置了 guidbooldatetimedecimal、正则、长度范围和数字范围等约束。

Controller 还是 Minimal API

ASP.NET Core 给了两种定义端点的方式:传统 Controller 和 Minimal API,各有适用场景。

Controller 继承自 ControllerBase,适合端点多、有复杂过滤器和横切关注点的 API。[ApiController] 特性开启自动模型验证、绑定源推断和标准化 ProblemDetails 错误响应。Controller 与 Action Filter、Area 和 OpenAPI 文档工具天然集成。

Minimal API 把 lambda 或方法组直接映射到路由,没有那么多仪式感,适合微服务、Azure Functions 风格端点,或者想要最小启动开销的项目。.NET 10 让 Minimal API 更符合人体工学,OpenAPI 生成也更完善。代价是 Action Filter 等高级功能需要更多手动接线。

从端点数量不多的全新 API 来说,Minimal API 够用;端点一旦超过十几个,Controller 在代码组织上通常更好扩展。两者可以在同一个应用里混用,不是互斥选择。

认证与授权

安全不是可选项。ASP.NET Core 内置了 JWT bearer、Cookie、API Key、OAuth 2.0 和 OpenID Connect 的支持。对于现代 Web API,由身份提供商(Azure Active Directory、Auth0 或自托管 IdentityServer)签发的 JWT bearer token 是主流方案。

ASP.NET Core 的授权是基于策略的。在 AddAuthorization(...) 里定义策略,用 [Authorize(Policy = "PolicyName")] 应用。策略可以要求特定角色、Claims、Scope,或者你自己实现的自定义要求。这比在几十个控制器上散布 [Authorize(Roles = "Admin")] 更容易维护:

[ApiController]
[Route("api/[controller]")]
[Authorize]
public class OrdersController : ControllerBase
{
    [HttpGet]
    public IActionResult GetAll() => Ok(Array.Empty<object>());

    [HttpGet("{id:guid}")]
    public IActionResult GetById(Guid id) => Ok(new { Id = id });

    [HttpPost]
    [Authorize(Policy = "AdminOnly")]
    public IActionResult Create([FromBody] CreateOrderRequest request)
        => CreatedAtAction(nameof(GetById), new { id = Guid.NewGuid() }, request);

    [HttpDelete("{id:guid}")]
    [AllowAnonymous]
    public IActionResult Delete(Guid id) => NoContent();
}

public record CreateOrderRequest(string CustomerId, decimal Total);

生产环境里的 JWT 签名密钥和连接字符串,用 Azure Key Vault 或 ASP.NET Core Data Protection 管理,不要放进 appsettings.json

API 版本管理

API 会随时间演进,客户端不可能总是和服务端同步升级。API 版本管理让你引入破坏性变更时不打断现有消费方。

三种常见方式:URL 版本(/api/v1/products)、查询字符串(/api/products?api-version=1.0)和请求头(Api-Version: 1.0)。URL 版本最显眼,也最容易在浏览器或 curl 里测试。Asp.Versioning.Http NuGet 包对三种方式都有干净的支持,并能自动生成每个版本独立的 OpenAPI 文档。

在版本管理上,越早引入越省事。就算你从没发布 v2,以 api/v1/ 开头也向消费方传达了一个信号:你考虑过这件事,未来有清晰的升级路径。

统一错误处理与 ProblemDetails

错误响应不一致是 API 消费方的噩梦。RFC 9457 定义了 ProblemDetails 格式——一个描述 HTTP API 错误的标准 JSON 结构。ASP.NET Core 内置了支持,[ApiController] 会自动对 400 响应启用它。

.NET 8 引入的 IExceptionHandler 接口配合 AddExceptionHandler<T>()UseExceptionHandler() 提供集中异常处理,完全控制响应格式:

public sealed class GlobalExceptionHandler : IExceptionHandler
{
    private readonly ILogger<GlobalExceptionHandler> _logger;

    public GlobalExceptionHandler(ILogger<GlobalExceptionHandler> logger)
    {
        _logger = logger;
    }

    public async ValueTask<bool> TryHandleAsync(
        HttpContext httpContext,
        Exception exception,
        CancellationToken cancellationToken)
    {
        _logger.LogError(exception, "Unhandled exception: {Message}", exception.Message);

        var (status, title) = exception switch
        {
            NotFoundException => (StatusCodes.Status404NotFound, "Resource not found"),
            ValidationException => (StatusCodes.Status422UnprocessableEntity, "Validation failed"),
            _ => (StatusCodes.Status500InternalServerError, "An unexpected error occurred")
        };

        var problemDetails = new ProblemDetails
        {
            Status = status,
            Title = title,
            Detail = exception.Message
        };

        httpContext.Response.StatusCode = status;
        await httpContext.Response.WriteAsJsonAsync(problemDetails, cancellationToken);

        return true;
    }
}

所有未处理异常都会流经这个处理器,产生一致的 JSON 响应。消费方拿到的是标准的 typetitlestatusdetailinstance 字段——不再需要猜测错误响应究竟是纯字符串、自定义 JSON 对象还是 HTML 页面。

实践建议:Action 方法只处理正常路径,预期失败抛领域异常(NotFoundExceptionConflictException 等),全局处理器负责翻译。Action 里不需要 try-catch 块,代码干净很多。

模型验证

[ApiController] 特性会自动验证传入的请求模型。如果验证失败,直接返回 400 Bad Request 加 ProblemDetails 响应体,不需要在 Action 里写一行验证代码。

在请求记录或类上加 [Required][MaxLength(200)][Range(0.01, 999999.99)][Url] 等特性就够了。客户端漏传必填字段或值超出范围,ASP.NET Core 立即返回 400 并列出每个字段的错误详情。不需要 if (!ModelState.IsValid) 守卫语句。

复杂的验证逻辑——跨字段校验、数据库唯一性检查——可以用 FluentValidation。它通过 IValidator<T> 抽象与 ASP.NET Core 的验证管道干净集成。

中间件处理横切关注点

中间件适合处理跨多个端点的广泛关注点:关联 ID、限流、响应压缩、HTTPS 重定向、请求/响应日志。写自定义中间件很直接,实现一个带 InvokeAsync(HttpContext context, RequestDelegate next) 方法的类,然后用 app.UseMiddleware<YourMiddleware>() 注册即可。

Serilog 的请求日志中间件是结构化日志的自然选择,每个请求自动记录 HTTP 方法、路径、状态码和耗时,不需要在每个 Action 里手写。

Mediator 模式也和 Web API 控制器搭配得很好。Action 方法变成薄薄的调度层,把命令和查询发给 Mediator,控制器专注 HTTP 关注点,业务逻辑推到专用的 Handler 类。这有时被称为 CQRS-over-HTTP 模式,随着 API 规模增长,扩展性很好。

集成测试

WebApplicationFactory<TProgram> 做集成测试是 ASP.NET Core API 的黄金标准。它在内存中启动整个应用——包含中间件、DI 注册、路由、序列化——然后让你用 HttpClient 发真实的 HTTP 请求。这种方式能发现隔离单元测试发现不了的问题:路由配置错误、中间件顺序 bug、序列化边界情况。

public class ProductsControllerTests : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly HttpClient _client;

    public ProductsControllerTests(WebApplicationFactory<Program> factory)
    {
        _client = factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                services.AddSingleton<IProductService, FakeProductService>();
            });
        }).CreateClient();
    }

    [Fact]
    public async Task GetProducts_ReturnsOk()
    {
        var response = await _client.GetAsync("/api/products");
        response.StatusCode.ShouldBe(HttpStatusCode.OK);
    }

    [Fact]
    public async Task CreateProduct_InvalidBody_Returns400()
    {
        var body = new StringContent("{}", Encoding.UTF8, "application/json");
        var response = await _client.PostAsync("/api/products", body);
        response.StatusCode.ShouldBe(HttpStatusCode.BadRequest);
    }
}

在 CI 里每次 Pull Request 都跑这些测试。一个破损的 API 合约在到达生产之前被发现,修复成本远低于消费方报错之后再处理。

实践上,用 WithWebHostBuilder 替换真实数据库为内存实现,仓储层测试可以用 SQLite 内存数据库,速度快而且隔离。

部署:Docker、Azure 和 Native AOT

ASP.NET Core 应用容器化很干净。官方基础镜像 mcr.microsoft.com/dotnet/aspnet:10.0 是起点。结合 .NET 10 的 Native AOT,可以产出自包含的二进制容器镜像,启动时间低于一秒——对按冷启动计费的容器部署来说是显著优势。.NET 10 SDK 的 PublishAot 属性自动化了这个流程。

Azure App Service 是最简单的部署目标:推送代码或 Docker 镜像,连接字符串配置为 App Settings,就完成了。需要自动扩缩容、边车容器、流量分割等高级场景时,Azure Container Apps 提供托管的类 Kubernetes 环境,不需要自己运维。

如果项目在成长为更大的分布式系统,在扩展之前先想清楚架构模式很重要。从单体起步、按模块划分边界的模块化单体架构,是全面微服务之前一个结构清晰的中间地带,对 ASP.NET Core Web API 项目的拆分方式有直接影响。

常见问题

ASP.NET Core Web API 和 ASP.NET Core MVC 的区别?

MVC 面向服务端渲染 HTML 的 Web 应用,包含 Razor 视图、Tag Helper、ViewData/TempData 和完整的 Controller 基类。Web API 专注于向非浏览器客户端——移动端、JavaScript 前端、其他服务——返回结构化数据(JSON、XML)。Web API 里继承 ControllerBase 而不是 Controller,排除了视图引擎的开销。两者共享相同的中间件管道、路由引擎和依赖注入基础设施。

错误如何统一处理?

以 RFC 9457 ProblemDetails 标准为基础。为领域错误定义具体的异常类型(NotFoundExceptionConflictExceptionValidationException),在全局 IExceptionHandler 里处理它们。Action 方法保持干净,只走正常路径;所有预期失败抛领域异常,意外错误冒泡到全局处理器。消费方只需要写一套错误处理逻辑。

如何在不破坏现有客户端的情况下版本化 API?

URL 路径版本(/api/v1/)是相对安全的选项:显式、不需要特殊请求头、任何 HTTP 工具都能理解。从第一天起就加上,而不是等 API 成熟后再补。引入破坏性变更时创建新版本,保留旧版本直到客户端迁移完成。非破坏性的添加性变更不需要版本号升级。

如何为集成测试替换真实依赖?

factory.WithWebHostBuilder 里的 ConfigureServices 覆盖服务注册,把生产实现换成测试替身。数据库依赖可以用 SQLite 内存模式,速度快且每次测试都是干净状态。


如果你关注 AI 助手、开发工具和软件工程实践,可以关注 Aide Hub。这里会继续分享能落地的工具教程、技术观察和项目经验。

参考


Tags


Next

.NET 10 Native AOT 下让反射保持安全:DynamicallyAccessedMembers 实用指南