
今天咱们来回顾并深入探讨 .NET 9 下的现代化 Web API 构建之道。
第一章:基石搭建 —— .NET 9 环境、项目结构与现代化配置
1.1 技术选型与核心理念
到了2026年,构建 Web API 早已不是简单地“跑通 Hello World”。你得考虑云原生亲和性、冷启动速度、内存消耗,甚至 AI 交互能力。为此,我们选定的核心技术栈如下:
- 运行时: .NET 9 (Latest LTS)
- 语言: C# 14 (引入了更强大的模式匹配、主构造函数增强、集合表达式等特性)
- API 风格: Minimal API (完全摒弃
[ApiController] 模板,追求极致的性能与代码简洁性)
- 架构模式: 垂直切片架构 (Vertical Slice Architecture) + 模块化单体 (Modular Monolith) 起步,为未来拆分为微服务预留接口。
- 数据访问: EF Core 9 (利用其无跟踪查询等优化) + Dapper (应对特定高性能场景)
- 文档: OpenAPI 3.1 (框架内置支持,优先使用
Scalar 作为 API 文档浏览器,替代旧版 Swagger UI)
- 验证: FluentValidation 集成
- 映射: Mapster (相比 AutoMapper 更快,因为它在编译时生成代码)
- 日志与监控: OpenTelemetry (原生集成) + Prometheus + Grafana
- 容器化: Docker (采用多阶段构建) + Native AOT (专为 Serverless 等高要求场景设计)
为什么选择 Minimal API 而非传统 Controller 呢?根据微软近年的基准测试数据,.NET 9 的 Minimal API 在请求吞吐量上比传统 Controller 模式高出 15%-20%,内存分配也减少了约 30%。它消除了反射带来的开销,让代码更贴近 HTTP 协议的本质。
1.2 环境准备与工具链
在动手编码前,确保你的开发环境满足以下标准:
- .NET 9 SDK: 版本需 >= 9.0.300 (当前最新稳定版)。
- IDE:
- Visual Studio 2026 (v17.14+) 或 JetBrains Rider 2026.1。
- VS Code: 安装最新的 "C# Dev Kit" 和 "GitHub Copilot" 插件以辅助代码生成。
- 容器工具: Docker Desktop (建议启用 WSL2 后端) 或 Podman。
- 数据库: PostgreSQL 17 (推荐,因其出色的云原生友好性) 或 SQL Server 2025。
1.3 项目初始化:超越 dotnet new webapi
尽管 dotnet new webapi 能快速生成基础代码,但为了实施垂直切片架构,我们需要手动构建解决方案结构。这种结构按“功能”而非“技术层”组织代码,能极大地提升可维护性与可扩展性。
1.3.1 创建解决方案目录
打开终端,执行以下命令:
mkdir Net9ModernApi
cd Net9ModernApi
dotnet new sln -n Net9ModernApi
1.3.2 规划项目结构
我们将创建以下核心项目:
Net9ModernApi.Web: 入口项目,包含 Minimal API 端点定义、依赖注入配置及中间件管道。
Net9ModernApi.Core: 核心领域层,包含实体、值对象、领域事件以及接口定义(如 Repository 接口)。此项目应无外部依赖。
Net9ModernApi.Infrastructure: 基础设施层,包含 EF Core 上下文、Repository 实现、以及邮件、短信等外部服务的具体实现。
Net9ModernApi.Features: 核心创新所在。这是垂直切片的心脏。每个功能模块(如 Users, Products, Orders)在此作为一个独立的命名空间或子文件夹存在,包含该功能所需的 Request/Response DTO、Validator、以及 Endpoint 映射逻辑。
接下来,创建项目并设置依赖关系:
# 创建 Web 入口项目 (Minimal API 模板)
dotnet new web -n Net9ModernApi.Web -o src/Net9ModernApi.Web
# 创建核心类库
dotnet new classlib -n Net9ModernApi.Core -o src/Net9ModernApi.Core
# 创建基础设施类库
dotnet new classlib -n Net9ModernApi.Infrastructure -o src/Net9ModernApi.Infrastructure
# 创建功能模块类库
dotnet new classlib -n Net9ModernApi.Features -o src/Net9ModernApi.Features
# 将项目添加到解决方案
dotnet sln add src/Net9ModernApi.Web/Net9ModernApi.Web.csproj
dotnet sln add src/Net9ModernApi.Core/Net9ModernApi.Core.csproj
dotnet sln add src/Net9ModernApi.Infrastructure/Net9ModernApi.Infrastructure.csproj
dotnet sln add src/Net9ModernApi.Features/Net9ModernApi.Features.csproj
# 设置项目引用关系
# Web 引用 Features, Infrastructure
dotnet add src/Net9ModernApi.Web/Net9ModernApi.Web.csproj reference src/Net9ModernApi.Features/Net9ModernApi.Features.csproj
dotnet add src/Net9ModernApi.Web/Net9ModernApi.Web.csproj reference src/Net9ModernApi.Infrastructure/Net9ModernApi.Infrastructure.csproj
# Infrastructure 引用 Core
dotnet add src/Net9ModernApi.Infrastructure/Net9ModernApi.Infrastructure.csproj reference src/Net9ModernApi.Core/Net9ModernApi.Core.csproj
# Features 引用 Core (严格架构中,应通过依赖倒置,但此处为简化演示)
dotnet add src/Net9ModernApi.Features/Net9ModernApi.Features.csproj reference src/Net9ModernApi.Core/Net9ModernApi.Core.csproj
1.3.3 清理与配置 .csproj
现代 .NET 项目文件应极其简洁。我们需要启用 C# 14 特性,配置隐式 using,并设置可空引用类型。
修改 src/Net9ModernApi.Core/Net9ModernApi.Core.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>14.0</LangVersion>
<!-- 核心层不应有任何外部 NuGet 包依赖,保持纯净 -->
</PropertyGroup>
</Project>
修改 src/Net9ModernApi.Web/Net9ModernApi.Web.csproj:
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>14.0</LangVersion>
<!-- 开启 Native AOT 兼容性检查 (可选,为未来部署做准备) -->
<PublishAotCompatible>true</PublishAotCompatible>
</PropertyGroup>
<ItemGroup>
<!-- 开放 API 文档支持 (.NET 9 内置) -->
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.*" />
<!-- Scalar API 客户端 (替代旧版 SwaggerUI,更现代) -->
<PackageReference Include="Scalar.AspNetCore" Version="1.2.*" />
<!-- 其他依赖将在后续章节添加 -->
</ItemGroup>
</Project>
1.4 核心代码重构:打造极简入口
删除 Program.cs 中的默认模板代码,我们将从头构建一个清晰、分层的启动流程。
src/Net9ModernApi.Web/Program.cs 初始版本:
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Scalar.AspNetCore;
using Net9ModernApi.Web.Extensions; // 我们将在这里定义扩展方法
var builder = WebApplication.CreateBuilder(args);
// 1. 服务注册 (Service Registration)
// 将复杂的配置提取到扩展方法中,保持 Program.cs 清爽
builder.Services.AddCoreServices();
builder.Services.AddInfrastructureServices(builder.Configuration);
builder.Services.AddFeatureServices();
// 2. 配置 HTTP 管道 (Middleware Pipeline)
var app = builder.Build();
// 3. 配置中间件
app.ConfigureCustomMiddleware();
// 4. 映射 API 端点
app.MapFeatures();
// 5. 配置 OpenAPI / Scalar
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
app.MapScalarApiReference(options =>
{
options.Title = "Net9 Modern API";
options.DownloadOpenApi = true;
});
}
app.Run();
你会发现,Program.cs 现在只有声明式 (declarative) 的逻辑。具体的实现细节被隐藏在了 Extensions 命名空间下的静态类中。这是保持大型项目可读性的关键。
1.4.1 实现扩展方法占位符
在 src/Net9ModernApi.Web 下创建 Extensions 文件夹,并添加以下文件:
ServiceCollectionExtensions.cs:
using Microsoft.Extensions.DependencyInjection;
namespace Net9ModernApi.Web.Extensions;
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddCoreServices(this IServiceCollection services)
{
// 将来注册 Core 层的服务,如 MediatR (如果使用 CQRS)
// 目前 Core 层主要是 POCO 和接口,无需注册
return services;
}
public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
{
// 将来注册 EF Core, Redis, Email 服务等
var connectionString = configuration.GetConnectionString("DefaultConnection");
// 示例:预留 DB 上下文注册位置
// services.AddDbContext<AppDbContext>(options =>
// options.UseNpgsql(connectionString));
return services;
}
public static IServiceCollection AddFeatureServices(this IServiceCollection services)
{
// 扫描并注册 Features 层的服务
// 在垂直切片架构中,每个 Feature 可能有自己的 Handler 或 Validator
return services;
}
}
ApplicationBuilderExtensions.cs:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
namespace Net9ModernApi.Web.Extensions;
public static class ApplicationBuilderExtensions
{
public static IApplicationBuilder ConfigureCustomMiddleware(this IApplicationBuilder app)
{
// 全局异常处理中间件 (将在第三章详细实现)
// app.UseExceptionHandler("/error");
// 全局日志中间件
// app.UseCorrelationIdMiddleware();
// 静态文件 (如果需要)
// app.UseStaticFiles();
return app;
}
public static IEndpointRouteBuilder MapFeatures(this IEndpointRouteBuilder app)
{
// 这里是魔法发生的地方
// 我们将自动扫描 Features 程序集中的所有 IEndpointProvider 实现
// 或者手动调用各个模块的 MapEndpoints 方法
// 示例:健康检查
app.MapHealthChecks("/health");
// 示例:根路径
app.MapGet("/", () => Results.Ok(new { Message = "Welcome to .NET 9 Modern API", Version = "1.0.0", Time = DateTime.UtcNow }))
.WithName("GetRoot")
.WithOpenApi();
// 后续章节将展示如何动态加载 Feature 模块
// await app.MapUserFeatures();
// await app.MapProductFeatures();
return app;
}
}
1.5 第一个功能切片:健康检查与版本信息
为了验证架构是否跑通,我们不写传统的 Controller,而是直接在 Features 层定义一个最小的切片。
步骤 1: 在 Net9ModernApi.Features 中创建 Health 模块
创建文件夹结构:src/Net9ModernApi.Features/Health/
src/Net9ModernApi.Features/Health/HealthEndpoints.cs:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
namespace Net9ModernApi.Features.Health;
/// <summary>
/// 健康检查模块的端点定义
/// 遵循垂直切片:所有与健康相关的逻辑都在此
/// </summary>
public static class HealthEndpoints
{
/// <summary>
/// 将健康检查端点映射到路由
/// </summary>
public static IEndpointRouteBuilder MapHealthEndpoints(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/health")
.WithTags("Health")
.WithOpenApi();
group.MapGet("/live", HandleLiveCheck)
.WithName("LiveCheck")
.WithSummary("检查应用是否存活")
.Produces<string>(StatusCodes.Status200OK);
group.MapGet("/ready", HandleReadyCheck)
.WithName("ReadyCheck")
.WithSummary("检查应用是否就绪 (依赖项正常)")
.Produces<string>(StatusCodes.Status200OK)
.Produces<string>(StatusCodes.Status503ServiceUnavailable);
return app;
}
private static IResult HandleLiveCheck()
{
// 简单的存活检查,通常只要进程在跑就返回 OK
return Results.Ok("Alive");
}
private static async Task<IResult> HandleReadyCheck(IServiceProvider serviceProvider)
{
// 模拟依赖检查 (数据库、Redis等)
// 在实际项目中,这里会注入 HealthCheckService
await Task.Delay(10); // 模拟异步检查
// 假设一切正常
return Results.Ok("Ready");
}
}
步骤 2: 在 Web 层注册该模块
修改 src/Net9ModernApi.Web/Extensions/ApplicationBuilderExtensions.cs 中的 MapFeatures 方法:
using Net9ModernApi.Features.Health; // 引入命名空间
// ... inside MapFeatures method
app.MapHealthEndpoints(); // 注册健康检查模块
// ...
1.6 运行与验证
现在,让我们启动项目看看效果。
- 在终端进入
src/Net9ModernApi.Web 目录。
- 运行
dotnet run。
- 浏览器访问
https://localhost:5001/api/health/live。预期显示文本 "Alive"。
- 访问
https://localhost:5001/scalar/v1。你将看到一个现代化的 API 文档界面,自动列出了我们定义的接口。
1.7 章节总结与实践要点
- 目录结构与依赖纪律: 严禁在
Core 层引用 Infrastructure 或 Web。依赖方向必须是单向的:Web -> Features/Infrastructure -> Core。Features 文件夹内部应按业务域划分(如 Users, Orders),而不是按技术类型(如 Controllers, DTOs)。
- 善用 C# 14 特性: 广泛使用主构造函数、集合表达式和增强的模式匹配来减少样板代码,提升可读性。
- 配置管理: 不要硬编码连接字符串。使用
appsettings.json 结合 User Secrets (开发环境) 或环境变量 (生产环境)。
- 日志规范: 从一开始就使用结构化日志,例如
_logger.LogInformation("User {UserId} logged in at {Time}", userId, DateTime.UtcNow);,避免字符串拼接。
- 异常处理的伏笔: Minimal API 中的异常不会像 Controller 那样被
ApiController 特性自动捕获。我们需要一个全局的异常处理中间件,这部分将在后续章节深入探讨。
我们成功搭建了基于 .NET 9 和 C# 14 的现代化 Web API 骨架。这个骨架虽然简单,但具备了极强的扩展性。
第二章:数据持久化与领域建模 —— EF Core 9 实战与垂直切片的数据策略
2.1 架构决策:数据访问层的现代化演进
关于“是否还需要仓储模式(Repository Pattern)”的争论已经尘埃落定。官方架构指南和社区共识倾向于:对于大多数 Web API 场景,直接使用 DbContext 是更高效、更透明的选择。
为什么要摒弃传统通用仓储呢?因为像 IRepository<T> 这样的通用抽象往往会掩盖 ORM 的强大特性,容易导致泄露抽象和过度工程。而在垂直切片架构中,每个功能模块拥有自己精确的查询逻辑,直接注入 DbContext 或使用轻量级的 Query-Specific Repository 是更好的实践。
2.2 基础设施层搭建:EF Core 9 与 PostgreSQL
2.2.1 安装依赖
为 Infrastructure 和 Web 项目添加 NuGet 包:
# 进入 Infrastructure 项目目录
cd src/Net9ModernApi.Infrastructure
# 安装 EF Core 9 核心及 PostgreSQL 提供者
dotnet add package Microsoft.EntityFrameworkCore --version 9.0.*
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL --version 9.0.*
# 安装设计时工具 (用于迁移)
dotnet add package Microsoft.EntityFrameworkCore.Design --version 9.0.*
# 回到 Web 项目目录
cd ../Net9ModernApi.Web
2.2.2 定义领域实体 (Core Layer)
在 Net9ModernApi.Core 中,我们利用 C# 14 的主构造函数和不可变记录定义第一个业务实体:用户 (User)。
src/Net9ModernApi.Core/Entities/User.cs:
namespace Net9ModernApi.Core.Entities;
/// <summary>
/// 用户实体
/// 使用主构造函数初始化必填字段,确保对象创建即有效
/// </summary>
public class User
{
// 主构造函数
public User(string email, string fullName, string passwordHash)
{
Email = email;
FullName = fullName;
PasswordHash = passwordHash;
CreatedAt = DateTime.UtcNow;
Roles = new List<string>();
}
// 主键
public int Id { get; private set; }
// 常规属性
public string Email { get; private set; }
public string FullName { get; private set; }
public string PasswordHash { get; private set; }
public DateTime CreatedAt { get; private set; }
public DateTime? LastLoginAt { get; private set; }
public bool IsActive { get; private set; } = true;
// 导航属性:角色 (EF Core 9 支持方便的 JSON 列映射)
public List<string> Roles { get; private set; }
// 行为方法 (Domain Logic)
public void UpdateLastLogin()
{
LastLoginAt = DateTime.UtcNow;
}
public void Deactivate()
{
IsActive = false;
}
// 用于 EF Core 跟踪的空构造函数 (可选)
}
2.2.3 实现 DbContext
在 Net9ModernApi.Infrastructure 中创建持久化上下文。
src/Net9ModernApi.Infrastructure/Data/AppDbContext.cs:
using Microsoft.EntityFrameworkCore;
using Net9ModernApi.Core.Entities;
namespace Net9ModernApi.Infrastructure.Data;
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
{
}
public DbSet<User> Users => Set<User>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
// 配置 User 实体
modelBuilder.Entity<User>(entity =>
{
entity.ToTable("users"); // 小写表名,符合 PG 习惯
entity.HasKey(e => e.Id);
entity.Property(e => e.Email)
.IsRequired()
.HasMaxLength(255)
.HasColumnName("email");
entity.Property(e => e.FullName)
.IsRequired()
.HasMaxLength(100)
.HasColumnName("full_name");
entity.Property(e => e.PasswordHash)
.IsRequired()
.HasColumnName("password_hash");
entity.Property(e => e.CreatedAt)
.HasDefaultValueSql("CURRENT_TIMESTAMP")
.HasColumnName("created_at");
entity.Property(e => e.IsActive)
.HasDefaultValue(true)
.HasColumnName("is_active");
// 为 Email 建立唯一索引
entity.HasIndex(e => e.Email).IsUnique();
// 将 Roles 列表存储为 JSONB (是 EF Core 9 的亮点之一)
entity.OwnsMany(u => u.Roles, rb =>
{
rb.ToJson().HasColumnName("roles");
});
});
}
}
这里利用 entity.OwnsMany(...).ToJson() 将 Roles 列表直接映射为 PostgreSQL 的 JSONB 类型,避免了创建额外的关联表,同时保持了模型的简洁与灵活性。
2.3 依赖注入与配置连接
在 Net9ModernApi.Web 层,我们将 AppDbContext 注册到 DI 容器中。
修改 ServiceCollectionExtensions.cs:
using Microsoft.EntityFrameworkCore;
using Net9ModernApi.Infrastructure.Data;
namespace Net9ModernApi.Web.Extensions;
public static partial class ServiceCollectionExtensions
{
public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString("DefaultConnection")
?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
// 注册 DbContext,并启用重试机制以对抗云环境下的瞬时故障
services.AddDbContext<AppDbContext>(options =>
options.UseNpgsql(connectionString, npgsqlOptions =>
{
npgsqlOptions.MigrationsAssembly(typeof(AppDbContext).Assembly.FullName);
npgsqlOptions.EnableRetryOnFailure(
maxRetryCount: 5,
maxRetryDelay: TimeSpan.FromSeconds(30),
errorCodesToAdd: null);
});
);
return services;
}
}
2.4 实战:构建“创建用户”垂直切片
现在,我们来实践垂直切片架构的核心优势:高内聚。所有与创建用户相关的代码都放在同一个逻辑单元里。
2.4.1 定义 DTOs 和验证规则
src/Net9ModernApi.Features/Users/CreateUser/CreateUserRequest.cs:
using FluentValidation;
namespace Net9ModernApi.Features.Users.CreateUser;
// 请求 DTO
public record CreateUserRequest(
string Email,
string FullName,
string Password,
List<string>? Roles = null
);
// 验证器
public class CreateUserRequestValidator : AbstractValidator<CreateUserRequest>
{
public CreateUserRequestValidator()
{
RuleFor(x => x.Email)
.NotEmpty().WithMessage("邮箱不能为空")
.EmailAddress().WithMessage("邮箱格式不正确");
RuleFor(x => x.FullName)
.NotEmpty().WithMessage("姓名不能为空")
.MaximumLength(100).WithMessage("姓名不能超过 100 字符");
RuleFor(x => x.Password)
.NotEmpty().WithMessage("密码不能为空")
.MinimumLength(8).WithMessage("密码至少 8 位")
.Matches(@"[A-Z]+").WithMessage("密码必须包含大写字母")
.Matches(@"[a-z]+").WithMessage("密码必须包含小写字母")
.Matches(@"[0-9]+").WithMessage("密码必须包含数字");
}
}
2.4.2 实现业务逻辑 (Service)
src/Net9ModernApi.Features/Users/CreateUser/CreateUserService.cs:
using Net9ModernApi.Core.Entities;
using Net9ModernApi.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
namespace Net9ModernApi.Features.Users.CreateUser;
public class CreateUserService
{
private readonly AppDbContext _dbContext;
public CreateUserService(AppDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task<User> ExecuteAsync(CreateUserRequest request, CancellationToken ct)
{
// 1. 检查邮箱是否已存在
var existingUser = await _dbContext.Users
.AsNoTracking() // 只读查询,不跟踪
.FirstOrDefaultAsync(u => u.Email == request.Email, ct);
if (existingUser is not null)
{
throw new InvalidOperationException($"Email '{request.Email}' is already registered.");
}
// 2. 模拟密码哈希 (生产环境应使用 BCrypt 或 Argon2)
var passwordHash = FakeHash(request.Password);
// 3. 创建并保存用户
var user = new User(
email: request.Email.ToLower(),
fullName: request.FullName,
passwordHash: passwordHash
);
if (request.Roles is not null && request.Roles.Any())
{
user.Roles.Clear();
user.Roles.AddRange(request.Roles);
}
_dbContext.Users.Add(user);
await _dbContext.SaveChangesAsync(ct);
return user;
}
private static string FakeHash(string password)
{
return $"HASHED_{password}_SALT_123";
}
}
2.4.3 定义 API 端点
src/Net9ModernApi.Features/Users/CreateUser/CreateUserEndpoints.cs:
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Net9ModernApi.Infrastructure.Data;
namespace Net9ModernApi.Features.Users.CreateUser;
public static class CreateUserEndpoints
{
public static IEndpointRouteBuilder MapCreateUserEndpoint(this IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/users")
.WithTags("Users")
.WithOpenApi();
group.MapPost("/", HandleCreateUser)
.WithName("CreateUser")
.WithSummary("注册新用户")
.Accepts<CreateUserRequest>("application/json")
.Produces<User>(StatusCodes.Status201Created)
.ProducesProblem(StatusCodes.Status400BadRequest);
return app;
}
private static async Task<IResult> HandleCreateUser(
CreateUserRequest request,
IValidator<CreateUserRequest> validator,
CreateUserService service,
CancellationToken ct)
{
var validationResult = await validator.ValidateAsync(request, ct);
if (!validationResult.IsValid)
{
return Results.ValidationProblem(validationResult.ToDictionary());
}
try
{
var newUser = await service.ExecuteAsync(request, ct);
return Results.Created($"/api/users/{newUser.Id}", newUser);
}
catch (InvalidOperationException ex)
{
return Results.Conflict(new { message = ex.Message, code = "EMAIL_EXISTS" });
}
}
}
同时,别忘了在 Web 层的扩展方法中注册新的端点和服务。至此,一个完整的业务功能就实现了。你会发现,代码高度内聚,职责清晰,没有大量的分层文件。
第三章:系统的“免疫系统”与“眼睛” —— 全局异常处理、可观测性与中间件管道
3.1 统一错误响应格式
在 .NET 9 中,我们可以轻松实现符合 RFC 7807 标准的 ProblemDetails 错误响应。我们首先定义一个扩展了错误码的细节类。
src/Net9ModernApi.Core/Common/AppProblemDetails.cs:
using Microsoft.AspNetCore.Mvc;
namespace Net9ModernApi.Core.Common;
public class AppProblemDetails : ProblemDetails
{
public string? Code { get; set; }
public static AppProblemDetails Create(
string title,
int status,
string detail,
string? code = null)
{
return new AppProblemDetails
{
Title = title,
Status = status,
Detail = detail,
Code = code
};
}
}
3.2 全局异常处理中间件
这个中间件会捕获所有未被处理的异常,进行日志记录,并返回标准化 JSON。
src/Net9ModernApi.Web/Middleware/GlobalExceptionMiddleware.cs:
using System.Net;
using System.Text.Json;
using Net9ModernApi.Core.Common;
namespace Net9ModernApi.Web.Middleware;
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<GlobalExceptionMiddleware> _logger;
public GlobalExceptionMiddleware(RequestDelegate next, ILogger<GlobalExceptionMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
await HandleExceptionAsync(context, ex);
}
}
private async Task HandleExceptionAsync(HttpContext context, Exception exception)
{
var statusCode = HttpStatusCode.InternalServerError;
var title = "Internal Server Error";
var detail = "An unexpected error occurred.";
string? code = "INTERNAL_ERROR";
switch (exception)
{
case InvalidOperationException ioEx:
statusCode = HttpStatusCode.Conflict;
title = "Business Rule Violation";
detail = ioEx.Message;
code = "BUSINESS_ERROR";
break;
case KeyNotFoundException:
statusCode = HttpStatusCode.NotFound;
title = "Resource Not Found";
detail = exception.Message;
code = "NOT_FOUND";
break;
default:
_logger.LogError(exception, "Unhandled exception occurred for request {RequestPath}", context.Request.Path);
break;
}
var problemDetails = AppProblemDetails.Create(title, (int)statusCode, detail, code);
context.Response.ContentType = "application/problem+json";
context.Response.StatusCode = (int)statusCode;
var json = JsonSerializer.Serialize(problemDetails, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
await context.Response.WriteAsync(json);
}
}
注册时注意顺序:此中间件应被置于管道尽可能靠前的位置。
3.3 结构化日志与 CorrelationID
为了串联起一次请求在不同服务间的所有日志,我们在请求开始时就生成或透传一个唯一的 CorrelationId。
src/Net9ModernApi.Web/Middleware/CorrelationIdMiddleware.cs:
namespace Net9ModernApi.Web.Middleware;
public class CorrelationIdMiddleware
{
private readonly RequestDelegate _next;
private const string CorrelationIdHeader = "X-Correlation-ID";
public CorrelationIdMiddleware(RequestDelegate next) => _next = next;
public async Task InvokeAsync(HttpContext context, ILogger<CorrelationIdMiddleware> logger)
{
var correlationId = context.Request.Headers[CorrelationIdHeader].FirstOrDefault() ?? Guid.NewGuid().ToString("N");
context.Items["CorrelationId"] = correlationId;
// 将 ID 注入响应头和日志作用域
if (!context.Response.HasStarted)
{
context.Response.OnStarting(() =>
{
context.Response.Headers[CorrelationIdHeader] = correlationId;
return Task.CompletedTask;
});
}
using (logger.BeginScope(new Dictionary<string, object> { ["CorrelationId"] = correlationId }))
{
await _next(context);
}
}
}
3.4 深度健康检查
生产环境中,简单的存活检查是不够的,还需要知道数据库等依赖项是否就绪。.NET 提供了强大的健康检查中间件,支持检查 PostgreSQL、Redis 等。
在 ServiceCollectionExtensions.cs 中注册:
services.AddHealthChecks()
.AddNpgSql(configuration.GetConnectionString("DefaultConnection"), name: "postgres")
.AddCheck("self", () => HealthCheckResult.Healthy());
在端点映射中区分 Liveness 和 Readiness:
// Liveness: 仅检查进程是否卡死
app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = check => check.Tags.Contains("self") });
// Readiness: 检查所有依赖,决定 K8s 是否转发流量
app.MapHealthChecks("/health/ready", new HealthCheckOptions { Predicate = _ => true });
现在的 API 不仅有了业务功能,也具备了企业级应用所需的“免疫系统”和“观察能力”。
第四章:安全堡垒 —— 认证、授权与速率限制
4.1 JWT 认证实现
我们需要将密码哈希从模拟升级为 BCrypt,并构建完整的 Token 签发与刷新机制。
第一步:更新用户实体,增加 RefreshToken 和 RefreshTokenExpiryTime 属性。
第二步:配置 JWT 设置 (appsettings.json)。
"JwtSettings": {
"SecretKey": "YourSuperSecretKeyThatIsAtLeast32CharactersLong!2026",
"Issuer": "Net9ModernApi",
"Audience": "Net9ModernApiClient",
"AccessTokenExpirationMinutes": 15,
"RefreshTokenExpirationDays": 7
}
第三步:实现 AuthService来处理 Token 的生成与刷新。
我们创建一个包含生成 Access Token、Refresh Token 以及刷新逻辑的服务。
第四步:实现登录端点。
遵循垂直切片风格,创建一个 LoginEndpoints 类,POST 到 /api/auth/login,返回 TokenResponse。
4.2 高级授权
对于“用户只能编辑自己的资料”这类需求,我们实现一个基于策略的资源所有权授权处理器 IsResourceOwnerHandler。
它通过比对当前用户ID和资源ID,决定是否授权。
4.3 速率限制
为防止暴力破解,.NET 9 的内置速率限制可以很方便地使用。
我们在 Program.cs 中配置好策略,例如为登录接口设置每分钟仅允许5次的限制,然后通过 .RequireRateLimiting("LoginLimit") 应用到端点即可。
通过这一系列组合拳,API 的安全性得到了极大的提升。
第五章:极致性能与高级特性 —— Native AOT、分布式缓存与异步架构
5.1 Native AOT 编译
Native AOT 是 .NET 9 的杀手锏之一,它可以在发布时将 C# 代码直接编译为目标平台的机器码,从而带来数倍乃至数十倍的启动速度提升和大幅的内存节约。
配置的关键在于 .csproj 文件:
<PropertyGroup>
<PublishAot>true</PublishAot>
<!-- ... -->
</PropertyGroup>
同时,必须使用 JSON 源生成器来应对 AOT 对反射的限制:
[JsonSerializable(typeof(TokenResponse))]
[JsonSerializable(typeof(CreateUserRequest))]
public partial class AppJsonContext : JsonSerializerContext
{
}
发布后,你将得到一个无需安装 .NET 运行时、几十兆大小的独立可执行文件,其冷启动速度可以与 Go 或 Rust 程序相媲美。
5.2 Redis 分布式缓存
对于高频读取、低频写入的数据,引入 Redis 可以极大减轻数据库压力。
- 引入
Microsoft.Extensions.Caching.StackExchangeRedis 包。
- 封装一个
RedisCacheService,实现 ICacheService 接口,提供基于 JSON 序列化的 GetAsync 和 SetAsync 方法。
- 在查询逻辑中优先检查缓存,未命中再查库,并回写缓存。
5.3 后台任务
对于发送邮件等耗时操作,绝对不能阻塞 HTTP 请求。
我们可以基于 BackgroundService 和 Channel<T> 实现一个轻量级的后台任务队列。业务代码只需向队列中投入任务,即可立刻返回响应。
5.4 Server-Sent Events
对于服务端向客户端的实时推送需求,SSE 相比 WebSocket 更轻量,兼容性更好。
通过创建一个持有 Channel 的广播服务,并暴露一个 SSE 端点,前端就可以用标准 EventSource 接口接收到流式数据。
第六章:云原生部署与自动化运维
6.1 Docker 与 K8s
这里提供了一个生产级的 Docker Compose 文件,用于本地启动全套依赖(API应用、PostgreSQL、Redis、Prometheus、Grafana)。
同时,也提供了一个 K8s 的 Deployment 和 HPA 配置示例。特别注意的是,得益于 Native AOT 的低内存占用,我们可以将 Request 和 Limit 设置得很低,从而在相同的集群中部署更多 Pod,节省大量成本。
6.2 CI/CD
利用 GitHub Actions,我们可以轻松构建一条自动化流水线:
代码提交 -> 运行测试 -> Docker 构建 Native AOT 镜像 -> 推送至 GHCR。
关键在于 Dockerfile 需支持多阶段构建并在发布指令中启用 AOT 属性,流水线文件则负责完整编排这一过程。
全书总结
至此,我们完成了一个从架构设计、编码实现、到安全加固、性能优化,最终部署上云的完整生命周期。
回顾这趟旅程:
- 架构设计:我们选择了灵活的垂直切片架构,摆脱了传统分层的束缚。
- 数据持久化:借助 EF Core 和 PostgreSQL 的 JSONB 特性,实现了高效且灵活的数据访问。
- 系统健壮性:通过全局异常处理和深度健康检查,构建了应用的“免疫系统”。
- 可观测性:集成 OpenTelemetry,通过结构化日志和
CorrelationId 实现了全链路追踪。
- 安全性:搭建了 JWT 认证、细粒度授权和速率限制等多道防线。
- 性能:依托 Native AOT 编译和分布式缓存,将应用性能推向了极致。
- 运维部署:最终落地到 Docker 和 K8s 环境,并配置了自动化 CI/CD 流水线。
这个项目不仅是一个教程,更是一个生产就绪的现代化 云原生 微服务起点。你可以在 云栈社区 与更多开发者交流探讨,获取各类技术资源。
基础知识回顾,明天继续……

— 完 —