Chapter 04

ASP.NET Core Web API

掌握 Minimal API 与 Controller 两种风格,理解中间件管道、DI 生命周期和 OpenAPI 文档生成。

Minimal API:轻量高效的路由定义

Minimal API 是 .NET 6 引入的路由注册风格,无需 Controller 类,直接在 Program.cs 中用 Lambda 定义端点。它比传统 Controller 有更低的启动时间和内存开销,非常适合微服务和小型 API。

var builder = WebApplication.CreateBuilder(args);

// 注册服务
builder.Services.AddDbContext<AppDbContext>(opt =>
    opt.UseNpgsql(builder.Configuration.GetConnectionString("Default")));
builder.Services.AddScoped<IProductService, ProductService>();

var app = builder.Build();

// 路由组(共享前缀和过滤器)
var products = app.MapGroup("/api/products")
    .RequireAuthorization()            // 整组需要认证
    .WithTags("Products")              // OpenAPI 分组
    .WithOpenApi();

products.MapGet("/", async (IProductService svc, CancellationToken ct) =>
    Results.Ok(await svc.GetAllAsync(ct)));

products.MapGet("/{id:int}", async (int id, IProductService svc) =>
{
    var product = await svc.GetByIdAsync(id);
    return product is null
        ? Results.NotFound()
        : Results.Ok(product);
});

products.MapPost("/", async (
    CreateProductRequest req,
    IProductService svc,
    LinkGenerator links) =>
{
    var created = await svc.CreateAsync(req);
    return Results.CreatedAtRoute("GetProduct", new { id = created.Id }, created);
});

app.Run();

Controller-based API:企业级组织

对于大型项目,Controller 方式更易于组织和维护,支持过滤器、属性路由、模型验证等特性。

[ApiController]
[Route("api/[controller]")]
public class UsersController(IUserService userService) : ControllerBase
{
    // [ApiController] 自动:参数绑定、模型验证、400 响应

    [HttpGet]
    [ProducesResponseType(typeof(List<UserDto>), 200)]
    public async Task<IActionResult> GetAll(
        [FromQuery] int page = 1,
        [FromQuery] int pageSize = 20,
        CancellationToken ct = default)
    {
        var users = await userService.GetPagedAsync(page, pageSize, ct);
        return Ok(users);
    }

    [HttpPost]
    public async Task<IActionResult> Create([FromBody] CreateUserRequest req)
    {
        // ModelState 验证由 [ApiController] 自动处理
        var user = await userService.CreateAsync(req);
        return CreatedAtAction(nameof(GetById), new { id = user.Id }, user);
    }
}

// 请求 DTO(带数据注解验证)
public record CreateUserRequest(
    [Required, MaxLength(100)] string Name,
    [Required, EmailAddress]   string Email,
    [Range(18, 120)]           int    Age
);

中间件管道

ASP.NET Core 的请求处理是一个管道,每个中间件可以选择处理请求、修改响应,或调用 next 传递给下一个中间件。

请求 → [HTTPS Redirect] [Static Files] [Routing] [Authentication] [Authorization] [Rate Limiting] [自定义中间件 A] [自定义中间件 B] [Endpoint Handler] ← 实际处理逻辑 [自定义中间件 B] ← 响应返回时反向执行 [自定义中间件 A] ... ← 响应
// 自定义中间件(请求耗时记录)
public class RequestTimingMiddleware(RequestDelegate next, ILogger<RequestTimingMiddleware> logger)
{
    public async Task InvokeAsync(HttpContext context)
    {
        var sw = Stopwatch.StartNew();
        try
        {
            await next(context);
        }
        finally
        {
            sw.Stop();
            logger.LogInformation(
                "{Method} {Path} → {Status} in {Ms}ms",
                context.Request.Method,
                context.Request.Path,
                context.Response.StatusCode,
                sw.ElapsedMilliseconds);
        }
    }
}

// 注册中间件
app.UseMiddleware<RequestTimingMiddleware>();

依赖注入:三种生命周期

Singleton(单例)
整个应用生命周期只创建一个实例,所有请求共享。适用于无状态的工具类、配置对象、HttpClient(通过 IHttpClientFactory)、内存缓存等。注意:Singleton 中不能注入 Scoped 服务(会导致 Captive Dependency 问题)。
Scoped(作用域)
每个 HTTP 请求(或显式的 scope)创建一个实例,在该请求内共享。Entity Framework 的 DbContext 必须是 Scoped——保证一个请求内多个操作共享同一个数据库连接和事务上下文。
Transient(瞬态)
每次注入时创建新实例。适合轻量级、无状态的服务。如果一个 Transient 服务持有非托管资源(如数据库连接),必须实现 IDisposable,DI 容器在 scope 结束时会调用 Dispose。
builder.Services.AddSingleton<IMemoryCache, MemoryCache>();
builder.Services.AddScoped<AppDbContext>();            // EF Core DbContext
builder.Services.AddScoped<IUserRepository, UserRepository>();
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();

// 工厂注册(带逻辑)
builder.Services.AddSingleton<ILogger>(sp =>
    sp.GetRequiredService<ILoggerFactory>().CreateLogger("App"));

// HttpClient(内置连接池管理)
builder.Services.AddHttpClient<IGitHubClient, GitHubClient>(client =>
{
    client.BaseAddress = new Uri("https://api.github.com");
    client.DefaultRequestHeaders.Add("User-Agent", "MyApp");
}).AddStandardResilienceHandler();  // .NET 8+ 内置重试/熔断

配置系统

// appsettings.json
{
  "Database": {
    "ConnectionString": "Host=localhost;Database=app",
    "MaxRetry": 3
  },
  "Jwt": {
    "SecretKey": "${JWT_SECRET}",
    "ExpiryMinutes": 60
  }
}

// 强类型配置(Options Pattern)
public class JwtOptions
{
    public const string Section = "Jwt";
    public string SecretKey { get; set; } = string.Empty;
    public int    ExpiryMinutes { get; set; } = 60;
}

// 注册
builder.Services.Configure<JwtOptions>(builder.Configuration.GetSection(JwtOptions.Section));

// 注入使用
public class TokenService(IOptions<JwtOptions> options)
{
    private readonly JwtOptions _opt = options.Value;
    // ...
}

全局异常处理

// .NET 8+ Problem Details 规范(RFC 9457)
builder.Services.AddProblemDetails();
builder.Services.AddExceptionHandler<GlobalExceptionHandler>();

public class GlobalExceptionHandler : IExceptionHandler
{
    public async ValueTask<bool> TryHandleAsync(
        HttpContext ctx,
        Exception ex,
        CancellationToken ct)
    {
        (int status, string title) = ex switch
        {
            ValidationException     => (400, "Validation Error"),
            NotFoundException       => (404, "Not Found"),
            UnauthorizedException   => (401, "Unauthorized"),
            _                       => (500, "Internal Server Error")
        };

        ctx.Response.StatusCode = status;
        await ctx.Response.WriteAsJsonAsync(new ProblemDetails
        {
            Status = status,
            Title  = title,
            Detail = ex.Message
        }, ct);

        return true;
    }
}

OpenAPI / Scalar(.NET 9 默认 UI)

.NET 9 内置了 OpenAPI 文档生成,并将 Scalar 作为默认 UI 替代 Swagger UI,界面更现代。

builder.Services.AddOpenApi();  // .NET 9 内置,无需 Swashbuckle

if (app.Environment.IsDevelopment())
{
    app.MapOpenApi();         // 生成 /openapi/v1.json
    app.MapScalarApiReference();  // Scalar UI at /scalar/v1
}

// 丰富 OpenAPI 描述
app.MapGet("/api/users/{id}", async (int id, IUserService svc) => { /*...*/ })
    .WithName("GetUser")
    .WithSummary("获取用户详情")
    .WithDescription("根据 ID 获取单个用户的完整信息")
    .Produces<UserDto>(200)
    .Produces(404)
    .RequireAuthorization();
本章小结 Minimal API 适合小型/微服务场景,Controller 适合大型项目。中间件管道的顺序至关重要——认证必须在授权之前,路由必须在端点执行之前。DI 的三种生命周期中,DbContext 必须 Scoped,HttpClient 通过工厂管理。全局异常处理配合 Problem Details 规范,为 API 消费者提供一致的错误格式。.NET 9 内置 OpenAPI + Scalar,零配置即可获得交互式 API 文档。