找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3566

积分

0

好友

473

主题
发表于 1 小时前 | 查看: 4| 回复: 0

一只像素风格的白鸽在黑色背景上展翅飞翔,翅膀完全张开,尾羽细节清晰

今天咱们来回顾并深入探讨 .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 环境准备与工具链

在动手编码前,确保你的开发环境满足以下标准:

  1. .NET 9 SDK: 版本需 >= 9.0.300 (当前最新稳定版)。
    • 检查命令:dotnet --version
  2. IDE:
    • Visual Studio 2026 (v17.14+) 或 JetBrains Rider 2026.1
    • VS Code: 安装最新的 "C# Dev Kit" 和 "GitHub Copilot" 插件以辅助代码生成。
  3. 容器工具: Docker Desktop (建议启用 WSL2 后端) 或 Podman。
  4. 数据库: 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 规划项目结构

我们将创建以下核心项目:

  1. Net9ModernApi.Web: 入口项目,包含 Minimal API 端点定义、依赖注入配置及中间件管道。
  2. Net9ModernApi.Core: 核心领域层,包含实体、值对象、领域事件以及接口定义(如 Repository 接口)。此项目应无外部依赖
  3. Net9ModernApi.Infrastructure: 基础设施层,包含 EF Core 上下文、Repository 实现、以及邮件、短信等外部服务的具体实现。
  4. 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 运行与验证

现在,让我们启动项目看看效果。

  1. 在终端进入 src/Net9ModernApi.Web 目录。
  2. 运行 dotnet run
  3. 浏览器访问 https://localhost:5001/api/health/live。预期显示文本 "Alive"。
  4. 访问 https://localhost:5001/scalar/v1。你将看到一个现代化的 API 文档界面,自动列出了我们定义的接口。

1.7 章节总结与实践要点

  1. 目录结构与依赖纪律: 严禁在 Core 层引用 InfrastructureWeb。依赖方向必须是单向的:Web -> Features/Infrastructure -> CoreFeatures 文件夹内部应按业务域划分(如 Users, Orders),而不是按技术类型(如 Controllers, DTOs)。
  2. 善用 C# 14 特性: 广泛使用主构造函数、集合表达式和增强的模式匹配来减少样板代码,提升可读性。
  3. 配置管理: 不要硬编码连接字符串。使用 appsettings.json 结合 User Secrets (开发环境) 或环境变量 (生产环境)。
  4. 日志规范: 从一开始就使用结构化日志,例如 _logger.LogInformation("User {UserId} logged in at {Time}", userId, DateTime.UtcNow);,避免字符串拼接。
  5. 异常处理的伏笔: 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 签发与刷新机制。

第一步:更新用户实体,增加 RefreshTokenRefreshTokenExpiryTime 属性。

第二步:配置 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 可以极大减轻数据库压力。

  1. 引入 Microsoft.Extensions.Caching.StackExchangeRedis 包。
  2. 封装一个 RedisCacheService,实现 ICacheService 接口,提供基于 JSON 序列化的 GetAsyncSetAsync 方法。
  3. 在查询逻辑中优先检查缓存,未命中再查库,并回写缓存。

5.3 后台任务

对于发送邮件等耗时操作,绝对不能阻塞 HTTP 请求。
我们可以基于 BackgroundServiceChannel<T> 实现一个轻量级的后台任务队列。业务代码只需向队列中投入任务,即可立刻返回响应。

5.4 Server-Sent Events

对于服务端向客户端的实时推送需求,SSE 相比 WebSocket 更轻量,兼容性更好。
通过创建一个持有 Channel 的广播服务,并暴露一个 SSE 端点,前端就可以用标准 EventSource 接口接收到流式数据。

第六章:云原生部署与自动化运维

6.1 Docker 与 K8s

这里提供了一个生产级的 Docker Compose 文件,用于本地启动全套依赖(API应用、PostgreSQL、Redis、Prometheus、Grafana)。
同时,也提供了一个 K8s 的 DeploymentHPA 配置示例。特别注意的是,得益于 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 流水线。

这个项目不仅是一个教程,更是一个生产就绪的现代化 云原生 微服务起点。你可以在 云栈社区 与更多开发者交流探讨,获取各类技术资源。

基础知识回顾,明天继续……

一张浅米色背景的书法作品局部特写,隐约可见'读'、'南'、'山'等字迹,充满文化气息

— 完 —




上一篇:iPhone 17 全系价格新低:标准版 4999 元,Pro Max 8599 元
下一篇:Anthropic 官方复盘:三个并发 Bug 叠加导致 Claude Code 性能回退,已修复并重置配额
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-5-1 02:08 , Processed in 1.271337 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表