From 0cbb5a2c0e9e1736becb41a0ed506de548f07468 Mon Sep 17 00:00:00 2001 From: movingsam Date: Sat, 28 Feb 2026 15:44:16 +0800 Subject: [PATCH] docs: map YARP gateway codebase --- .planning/codebase/ARCHITECTURE.md | 457 ++++++++++++++++ .planning/codebase/CONCERNS.md | 499 +++++++++++++++++ .planning/codebase/CONVENTIONS.md | 690 ++++++++++++++++++++++++ .planning/codebase/INTEGRATIONS.md | 374 +++++++++++++ .planning/codebase/STACK.md | 189 +++++++ .planning/codebase/STRUCTURE.md | 465 ++++++++++++++++ .planning/codebase/TESTING.md | 833 +++++++++++++++++++++++++++++ 7 files changed, 3507 insertions(+) create mode 100644 .planning/codebase/ARCHITECTURE.md create mode 100644 .planning/codebase/CONCERNS.md create mode 100644 .planning/codebase/CONVENTIONS.md create mode 100644 .planning/codebase/INTEGRATIONS.md create mode 100644 .planning/codebase/STACK.md create mode 100644 .planning/codebase/STRUCTURE.md create mode 100644 .planning/codebase/TESTING.md diff --git a/.planning/codebase/ARCHITECTURE.md b/.planning/codebase/ARCHITECTURE.md new file mode 100644 index 0000000..ca999ef --- /dev/null +++ b/.planning/codebase/ARCHITECTURE.md @@ -0,0 +1,457 @@ +# YARP Gateway 架构文档 + +## 1. 整体架构模式 + +本项目基于 **YARP (Yet Another Reverse Proxy)** 实现的 API 网关,采用 **反向代理模式**,支持多租户路由、动态配置和分布式负载均衡。 + +### 1.1 架构图 + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 外部请求 │ +└─────────────────────────────────┬───────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ ASP.NET Core Pipeline │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ ┌────────────────┐ ┌─────────────────────┐ ┌──────────────────────┐ │ +│ │ CORS 中间件 │ -> │ JwtTransformMiddleware │ -> │ TenantRoutingMiddleware │ │ +│ └────────────────┘ └─────────────────────┘ └──────────────────────┘ │ +└─────────────────────────────────┬───────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ YARP Reverse Proxy │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ ┌───────────────────────────┐ ┌──────────────────────────────────────┐ │ +│ │ DynamicProxyConfigProvider │ -> │ DistributedWeightedRoundRobinPolicy │ │ +│ └───────────┬───────────────┘ └──────────────────────────────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌───────────────────────────────────────────────────────────────────────┐ │ +│ │ RouteConfig / ClusterConfig │ │ +│ └───────────────────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────┬───────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────────────────────┐ +│ 后端服务集群 │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Service A│ │ Service B│ │ Service C│ │ Service D│ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### 1.2 核心设计模式 + +| 模式 | 应用场景 | 实现位置 | +|------|----------|----------| +| 反向代理 | 请求转发 | `Yarp.ReverseProxy` | +| 策略模式 | 负载均衡策略 | `DistributedWeightedRoundRobinPolicy` | +| 观察者模式 | 配置变更监听 | `PgSqlConfigChangeListener` | +| 工厂模式 | DbContext 创建 | `GatewayDbContextFactory` | +| 单例模式 | 配置提供者 | `DatabaseRouteConfigProvider`, `DatabaseClusterConfigProvider` | +| 生产者-消费者 | 配置变更通知 | `Channel` in `PgSqlConfigChangeListener` | + +--- + +## 2. 核心组件和职责 + +### 2.1 中间件层 (Middleware) + +#### JwtTransformMiddleware +**文件路径**: `src/Middleware/JwtTransformMiddleware.cs` + +**职责**: +- 解析 JWT Token +- 提取租户信息 (tenant claim) +- 将用户信息注入请求头 + +**处理流程**: +``` +Authorization Header -> JWT 解析 -> 提取 Claims -> 注入 X-Tenant-Id, X-User-Id, X-User-Name, X-Roles +``` + +#### TenantRoutingMiddleware +**文件路径**: `src/Middleware/TenantRoutingMiddleware.cs` + +**职责**: +- 从请求头获取租户 ID +- 根据 URL 路径提取服务名称 +- 查询路由缓存获取目标集群 +- 设置动态集群 ID + +### 2.2 配置提供层 (Config Providers) + +#### DynamicProxyConfigProvider +**文件路径**: `src/DynamicProxy/DynamicProxyConfigProvider.cs` + +**职责**: +- 实现 YARP 的 `IProxyConfigProvider` 接口 +- 整合路由和集群配置 +- 提供配置变更通知机制 + +```csharp +public interface IProxyConfigProvider +{ + IProxyConfig GetConfig(); +} +``` + +#### DatabaseRouteConfigProvider +**文件路径**: `src/Config/DatabaseRouteConfigProvider.cs` + +**职责**: +- 从数据库加载路由配置 +- 转换为 YARP `RouteConfig` 格式 +- 支持热重载 + +#### DatabaseClusterConfigProvider +**文件路径**: `src/Config/DatabaseClusterConfigProvider.cs` + +**职责**: +- 从数据库加载集群配置 +- 管理服务实例 (地址、权重) +- 配置健康检查策略 + +### 2.3 服务层 (Services) + +#### RouteCache +**文件路径**: `src/Services/RouteCache.cs` + +**职责**: +- 内存缓存路由信息 +- 支持全局路由和租户专用路由 +- 提供快速查询接口 + +**数据结构**: +``` +_globalRoutes: ConcurrentDictionary // 全局路由 +_tenantRoutes: ConcurrentDictionary> // 租户路由 +``` + +**查询优先级**: 租户专用路由 > 全局路由 + +#### PgSqlConfigChangeListener +**文件路径**: `src/Services/PgSqlConfigChangeListener.cs` + +**职责**: +- 监听 PostgreSQL NOTIFY 事件 +- 双重保障:事件监听 + 轮询回退 +- 触发配置热重载 + +**监听流程**: +``` +PostgreSQL NOTIFY -> OnNotification -> _reloadChannel -> ReloadConfigAsync + │ + └── FallbackPollingAsync (5分钟轮询) +``` + +#### KubernetesPendingSyncService +**文件路径**: `src/Services/KubernetesPendingSyncService.cs` + +**职责**: +- 同步 Kubernetes 服务发现 +- 管理待处理服务列表 +- 清理过期服务记录 + +#### RedisConnectionManager +**文件路径**: `src/Services/RedisConnectionManager.cs` + +**职责**: +- 管理 Redis 连接 +- 提供分布式锁实现 +- 连接池管理 + +### 2.4 负载均衡层 + +#### DistributedWeightedRoundRobinPolicy +**文件路径**: `src/LoadBalancing/DistributedWeightedRoundRobinPolicy.cs` + +**职责**: +- 实现加权轮询负载均衡 +- 基于 Redis 的分布式状态存储 +- 支持实例权重配置 + +**算法流程**: +``` +1. 获取分布式锁 (Redis) +2. 读取负载均衡状态 +3. 计算权重选择目标 +4. 更新状态并释放锁 +5. 失败时降级到简单选择 +``` + +--- + +## 3. 数据流和请求处理流程 + +### 3.1 请求处理流程图 + +```mermaid +sequenceDiagram + participant Client as 客户端 + participant CORS as CORS中间件 + participant JWT as JwtTransformMiddleware + participant Tenant as TenantRoutingMiddleware + participant YARP as YARP代理 + participant LB as 负载均衡器 + participant Service as 后端服务 + + Client->>CORS: HTTP请求 + CORS->>JWT: 跨域检查通过 + JWT->>JWT: 解析JWT Token + JWT->>Tenant: 注入租户信息头 + Tenant->>Tenant: 提取服务名称 + Tenant->>Tenant: 查询RouteCache + Tenant->>YARP: 设置动态集群ID + YARP->>LB: 获取可用目标 + LB->>LB: 加权轮询选择 + LB->>Service: 转发请求 + Service-->>Client: 返回响应 +``` + +### 3.2 配置变更流程 + +```mermaid +flowchart TD + A[数据库变更] --> B[SaveChangesAsync] + B --> C[DetectConfigChanges] + C --> D[NOTIFY gateway_config_changed] + D --> E[PgSqlConfigChangeListener] + E --> F{收到通知?} + F -->|是| G[ReloadConfigAsync] + F -->|否| H[轮询检测版本变化] + H --> G + G --> I[RouteCache.ReloadAsync] + G --> J[DatabaseRouteConfigProvider.ReloadAsync] + G --> K[DatabaseClusterConfigProvider.ReloadAsync] + I --> L[更新内存缓存] + J --> L + K --> L + L --> M[DynamicProxyConfigProvider.UpdateConfig] + M --> N[触发 IChangeToken] + N --> O[YARP重新加载配置] +``` + +### 3.3 Kubernetes 服务发现流程 + +``` +┌─────────────────┐ +│ Kubernetes API │ +└────────┬────────┘ + │ 30s 间隔 + ▼ +┌─────────────────────────────┐ +│ KubernetesPendingSyncService │ +├─────────────────────────────┤ +│ 1. 获取 K8s 服务列表 │ +│ 2. 对比现有待处理记录 │ +│ 3. 新增/更新/清理记录 │ +└────────┬────────────────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ GwPendingServiceDiscovery │ +│ (待处理服务发现表) │ +└────────┬────────────────────┘ + │ + ▼ +┌─────────────────────────────┐ +│ PendingServicesController │ +│ - GET: 查看待处理服务 │ +│ - POST /assign: 分配集群 │ +│ - POST /reject: 拒绝服务 │ +└─────────────────────────────┘ +``` + +--- + +## 4. 关键抽象层 + +### 4.1 配置模型 + +``` +┌───────────────────────────────────────────────────────────────┐ +│ 配置层次结构 │ +├───────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ JwtConfig │ │ RedisConfig │ │ +│ │ - Authority │ │ - Connection │ │ +│ │ - Audience │ │ - Database │ │ +│ │ - Validate* │ │ - InstanceName │ │ +│ └─────────────────┘ └─────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ DynamicProxyConfigProvider │ │ +│ │ ┌─────────────────┐ ┌─────────────────┐ │ │ +│ │ │ RouteConfig[] │ │ ClusterConfig[] │ │ │ +│ │ │ - RouteId │ │ - ClusterId │ │ │ +│ │ │ - ClusterId │ │ - Destinations │ │ │ +│ │ │ - Match.Path │ │ - LoadBalancing │ │ │ +│ │ │ - Metadata │ │ - HealthCheck │ │ │ +│ │ └─────────────────┘ └─────────────────┘ │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +└───────────────────────────────────────────────────────────────┘ +``` + +### 4.2 数据模型 + +``` +┌─────────────────┐ ┌─────────────────┐ +│ GwTenant │ │ GwTenantRoute │ +├─────────────────┤ ├─────────────────┤ +│ Id │ │ Id │ +│ TenantCode ────┼────►│ TenantCode │ +│ TenantName │ │ ServiceName │ +│ Status │ │ ClusterId │ +│ Version │ │ PathPattern │ +│ IsDeleted │ │ Priority │ +└─────────────────┘ │ IsGlobal │ + │ Status │ + │ Version │ + └────────┬────────┘ + │ + ▼ + ┌─────────────────┐ + │ GwServiceInstance│ + ├─────────────────┤ + │ Id │ + │ ClusterId ────┤ + │ DestinationId │ + │ Address │ + │ Health │ + │ Weight │ + │ Status │ + │ Version │ + └─────────────────┘ +``` + +### 4.3 接口定义 + +```csharp +// 路由缓存接口 +public interface IRouteCache +{ + Task InitializeAsync(); + Task ReloadAsync(); + RouteInfo? GetRoute(string tenantCode, string serviceName); + RouteInfo? GetRouteByPath(string path); +} + +// Redis 连接管理接口 +public interface IRedisConnectionManager +{ + IConnectionMultiplexer GetConnection(); + Task AcquireLockAsync(string key, TimeSpan? expiry = null); + Task ExecuteInLockAsync(string key, Func> func, TimeSpan? expiry = null); +} + +// 负载均衡策略接口 (YARP) +public interface ILoadBalancingPolicy +{ + string Name { get; } + DestinationState? PickDestination(HttpContext context, ClusterState cluster, IReadOnlyList availableDestinations); +} +``` + +--- + +## 5. 入口点分析 + +### 5.1 程序入口 (`Program.cs`) + +**文件路径**: `src/Program.cs` + +**启动流程**: + +``` +1. 创建 WebApplication Builder + └── 配置 Serilog 日志 + +2. 配置选项 + ├── JwtConfig (JWT 认证配置) + └── RedisConfig (Redis 连接配置) + +3. 注册数据库服务 + └── GatewayDbContext (PostgreSQL) + +4. 注册核心服务 (Singleton) + ├── DatabaseRouteConfigProvider + ├── DatabaseClusterConfigProvider + ├── RouteCache + ├── RedisConnectionManager + ├── DynamicProxyConfigProvider + └── DistributedWeightedRoundRobinPolicy + +5. 注册后台服务 (HostedService) + ├── PgSqlConfigChangeListener + └── KubernetesPendingSyncService + +6. 配置中间件管道 + ├── CORS + ├── JwtTransformMiddleware + └── TenantRoutingMiddleware + +7. 映射端点 + ├── /health (健康检查) + ├── /api/gateway/* (管理 API) + └── /api/* (代理路由) + +8. 初始化并运行 + └── RouteCache.InitializeAsync() +``` + +### 5.2 依赖注入关系 + +``` +Program.cs + │ + ├── Config/ + │ ├── JwtConfig (Options) + │ ├── RedisConfig (Options + Singleton) + │ ├── DatabaseRouteConfigProvider (Singleton) + │ └── DatabaseClusterConfigProvider (Singleton) + │ + ├── DynamicProxy/ + │ └── DynamicProxyConfigProvider (Singleton, IProxyConfigProvider) + │ + ├── Services/ + │ ├── RouteCache (Singleton, IRouteCache) + │ ├── RedisConnectionManager (Singleton) + │ ├── PgSqlConfigChangeListener (HostedService) + │ └── KubernetesPendingSyncService (HostedService) + │ + ├── LoadBalancing/ + │ └── DistributedWeightedRoundRobinPolicy (Singleton, ILoadBalancingPolicy) + │ + └── Data/ + └── GatewayDbContext (DbContextFactory) +``` + +--- + +## 6. 技术栈 + +| 组件 | 技术 | 用途 | +|------|------|------| +| 反向代理 | YARP 2.x | 核心代理功能 | +| 数据库 | PostgreSQL + EF Core | 配置存储 | +| 缓存 | Redis | 分布式状态、锁 | +| 服务发现 | Fengling.ServiceDiscovery | Kubernetes 集成 | +| 日志 | Serilog | 结构化日志 | +| 容器化 | Docker | 部署支持 | +| 目标框架 | .NET 10.0 | 运行时 | + +--- + +## 7. 扩展点 + +1. **负载均衡策略**: 实现 `ILoadBalancingPolicy` 接口 +2. **配置提供者**: 继承 `IProxyConfigProvider` +3. **中间件**: 添加自定义中间件到管道 +4. **服务发现**: 扩展 `IServiceDiscoveryProvider` +5. **健康检查**: 配置 `HealthCheckConfig` \ No newline at end of file diff --git a/.planning/codebase/CONCERNS.md b/.planning/codebase/CONCERNS.md new file mode 100644 index 0000000..ea1b4fe --- /dev/null +++ b/.planning/codebase/CONCERNS.md @@ -0,0 +1,499 @@ +# YARP 网关项目技术债务与关注点分析 + +> 分析日期:2026-02-28 +> 分析范围:核心代码、配置、数据访问层 + +--- + +## 一、严重安全问题 🔴 + +### 1.1 硬编码凭据泄露 + +**文件位置:** `src/Config/RedisConfig.cs:5` +```csharp +public string ConnectionString { get; set; } = "81.68.223.70:16379,password=sl52788542"; +``` + +**问题描述:** Redis 连接字符串包含明文密码,直接硬编码在源代码中。此代码提交到版本控制系统后,密码将永久暴露。 + +**影响范围:** +- 攻击者获取代码后可直接访问 Redis 服务 +- 违反安全合规要求(如等保、GDPR) + +**改进建议:** +```csharp +// 使用环境变量或密钥管理服务 +public string ConnectionString { get; set; } = + Environment.GetEnvironmentVariable("REDIS_CONNECTION_STRING") ?? string.Empty; +``` + +--- + +### 1.2 配置文件凭据泄露 + +**文件位置:** `src/appsettings.json:19,28` +```json +"DefaultConnection": "Host=81.68.223.70;Port=15432;Database=fengling_gateway;Username=movingsam;Password=sl52788542" +"ConnectionString": "81.68.223.70:6379" +``` + +**问题描述:** 数据库连接字符串和 Redis 配置包含明文凭据,且这些配置文件通常会被提交到 Git 仓库。 + +**改进建议:** +- 使用 `appsettings.Development.json` 存储开发环境配置,并加入 `.gitignore` +- 生产环境使用环境变量或 Azure Key Vault / AWS Secrets Manager +- 敏感配置使用 `dotnet user-secrets` 管理 + +--- + +### 1.3 JWT 令牌未验证 + +**文件位置:** `src/Middleware/JwtTransformMiddleware.cs:39-40` +```csharp +var jwtHandler = new JwtSecurityTokenHandler(); +var jwtToken = jwtHandler.ReadJwtToken(token); +``` + +**问题描述:** 中间件仅**读取**JWT令牌,未进行签名验证、过期检查或颁发者验证。攻击者可伪造任意JWT令牌。 + +**影响范围:** +- 任何人可伪造租户ID、用户ID、角色信息 +- 可冒充任意用户访问系统 + +**改进建议:** +```csharp +// 应使用标准的 JWT 验证流程 +var validationParameters = new TokenValidationParameters +{ + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = _jwtConfig.Authority, + ValidAudience = _jwtConfig.Audience, + IssuerSigningKey = GetSigningKey() // 从配置获取公钥 +}; + +var principal = jwtHandler.ValidateToken(token, validationParameters, out _); +``` + +--- + +### 1.4 API 端点无认证保护 + +**文件位置:** `src/Controllers/GatewayConfigController.cs` 和 `src/Controllers/PendingServicesController.cs` + +**问题描述:** 所有管理API端点均未添加 `[Authorize]` 特性,任何人可直接调用: +- `POST /api/gateway/tenants` - 创建租户 +- `POST /api/gateway/routes` - 创建路由 +- `POST /api/gateway/clusters/{clusterId}/instances` - 添加服务实例 +- `POST /api/gateway/pending-services/{id}/assign` - 分配服务 + +**影响范围:** +- 攻击者可随意修改网关配置 +- 可注入恶意服务地址进行流量劫持 + +**改进建议:** +```csharp +[ApiController] +[Route("api/gateway")] +[Authorize(Roles = "Admin")] // 添加认证要求 +public class GatewayConfigController : ControllerBase +``` + +--- + +### 1.5 租户ID头部信任问题 + +**文件位置:** `src/Middleware/TenantRoutingMiddleware.cs:25` +```csharp +var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault(); +``` + +**问题描述:** 直接从请求头读取租户ID,未与JWT中的租户声明进行比对验证。攻击者可伪造 `X-Tenant-Id` 头部访问其他租户数据。 + +**改进建议:** +```csharp +// 从已验证的 JWT claims 中获取租户ID +var jwtTenantId = context.User.FindFirst("tenant")?.Value; +var headerTenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault(); + +if (!string.IsNullOrEmpty(jwtTenantId) && jwtTenantId != headerTenantId) +{ + // 记录安全事件 + _logger.LogWarning("Tenant ID mismatch: JWT={JwtTenant}, Header={HeaderTenant}", + jwtTenantId, headerTenantId); + context.Response.StatusCode = StatusCodes.Status403Forbidden; + return; +} +``` + +--- + +## 二、技术债务 🟠 + +### 2.1 ID生成策略问题 + +**文件位置:** `src/Controllers/GatewayConfigController.cs:484-487` +```csharp +private long GenerateId() +{ + return DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); +} +``` + +**问题描述:** 使用时间戳毫秒生成ID,在高并发场景下可能产生重复ID。 + +**改进建议:** +- 使用数据库自增主键(已有配置) +- 或使用雪花算法(Snowflake ID) +- 或使用 `Guid.NewGuid()` + +--- + +### 2.2 Redis连接重复初始化 + +**文件位置:** +- `src/Program.cs:39-60` - 注册 `IConnectionMultiplexer` +- `src/Services/RedisConnectionManager.cs:25-46` - 内部再次创建连接 + +**问题描述:** Redis连接被初始化两次,造成资源浪费和配置不一致风险。 + +**改进建议:** +```csharp +// Program.cs 中只注册一次 +builder.Services.AddSingleton(); +builder.Services.AddSingleton(sp => + sp.GetRequiredService().GetConnection()); +``` + +--- + +### 2.3 DTO 内嵌定义 + +**文件位置:** `src/Controllers/GatewayConfigController.cs:444-481` + +**问题描述:** 多个DTO类定义在Controller内部,不利于复用和测试。 + +**改进建议:** +- 将 DTO 移至 `src/DTOs/` 或 `src/Models/Dto/` 目录 +- 使用 Auto Mapper 或 Mapster 进行对象映射 + +--- + +### 2.4 魔法数字 + +**文件位置:** 多处使用数字常量 + +```csharp +// RouteCache.cs:99 +.Where(r => r.Status == 1 && !r.IsDeleted) + +// GatewayConfigController.cs:239 +route.Status = 1; + +// KubernetesPendingSyncService.cs:13 +private readonly TimeSpan _syncInterval = TimeSpan.FromSeconds(30); +``` + +**问题描述:** 状态值、超时时间等使用硬编码数字,降低代码可读性和可维护性。 + +**改进建议:** +```csharp +// 定义常量或枚举 +public static class RouteStatus +{ + public const int Active = 1; + public const int Inactive = 0; +} + +public static class ServiceConstants +{ + public static readonly TimeSpan DefaultSyncInterval = TimeSpan.FromSeconds(30); +} +``` + +--- + +### 2.5 异步方法命名不一致 + +**文件位置:** `src/Config/DatabaseRouteConfigProvider.cs:23` +```csharp +_ = LoadConfigAsync(); // Fire-and-forget without await +``` + +**问题描述:** 构造函数中调用异步方法但未等待完成,可能导致初始化竞态条件。 + +**改进建议:** +- 使用工厂模式异步初始化 +- 或在 `Program.cs` 中显式调用初始化方法 + +--- + +## 三、性能瓶颈风险 🟡 + +### 3.1 负载均衡锁竞争 + +**文件位置:** `src/LoadBalancing/DistributedWeightedRoundRobinPolicy.cs:48-53` +```csharp +var lockAcquired = db.StringSet( + lockKey, + lockValue, + TimeSpan.FromMilliseconds(500), + When.NotExists +); +``` + +**问题描述:** 每次请求都需要获取Redis分布式锁,高并发下会成为瓶颈。锁获取失败时降级策略不可靠。 + +**影响:** +- 单集群QPS受限 +- Redis延迟增加时网关吞吐量下降 + +**改进建议:** +- 考虑使用本地缓存 + 定期同步策略 +- 或使用一致性哈希算法避免锁需求 +- 增加本地计数器作为快速路径 + +--- + +### 3.2 路由缓存全量加载 + +**文件位置:** `src/Services/RouteCache.cs:94-137` +```csharp +var routes = await db.TenantRoutes + .Where(r => r.Status == 1 && !r.IsDeleted) + .ToListAsync(); +``` + +**问题描述:** 每次重载都清空并重新加载所有路由,大数据量下性能差。 + +**改进建议:** +- 实现增量更新机制 +- 使用版本号比对只更新变更项 +- 添加分页加载支持 + +--- + +### 3.3 数据库查询未优化 + +**文件位置:** `src/Controllers/GatewayConfigController.cs:145-148` +```csharp +var currentRouteVersion = await db.TenantRoutes + .OrderByDescending(r => r.Version) + .Select(r => r.Version) + .FirstOrDefaultAsync(stoppingToken); +``` + +**问题描述:** 每次轮询都执行 `ORDER BY` 查询获取最大版本号,缺少索引优化。 + +**改进建议:** +```sql +-- 添加索引 +CREATE INDEX IX_TenantRoutes_Version ON "TenantRoutes" ("Version" DESC); + +-- 或使用 MAX 聚合 +SELECT MAX("Version") FROM "TenantRoutes"; +``` + +--- + +### 3.4 PostgreSQL NOTIFY 连接管理 + +**文件位置:** `src/Data/GatewayDbContext.cs:72-75` +```csharp +using var connection = new NpgsqlConnection(connectionString); +connection.Open(); +using var cmd = new NpgsqlCommand($"NOTIFY {ConfigNotifyChannel.GatewayConfigChanged}", connection); +cmd.ExecuteNonQuery(); +``` + +**问题描述:** 每次保存变更都创建新的数据库连接发送通知,连接开销大。 + +**改进建议:** +- 使用连接池中的连接 +- 或复用 `PgSqlConfigChangeListener` 中的连接发送通知 + +--- + +## 四、脆弱区域 🟠 + +### 4.1 租户路由外键约束 + +**文件位置:** `src/Migrations/20260201120312_InitialCreate.cs:83-89` +```csharp +table.ForeignKey( + name: "FK_TenantRoutes_Tenants_TenantCode", + column: x => x.TenantCode, + principalTable: "Tenants", + principalColumn: "TenantCode", + onDelete: ReferentialAction.Restrict); +``` + +**问题描述:** `TenantRoutes.TenantCode` 有外键约束,但全局路由(`IsGlobal=true`)时 `TenantCode` 可为空字符串,可能导致数据一致性问题。 + +**改进建议:** +- 全局路由使用特定的占位符(如 "GLOBAL") +- 或修改外键约束为条件约束 + +--- + +### 4.2 健康检查配置硬编码 + +**文件位置:** `src/Config/DatabaseClusterConfigProvider.cs:77-86` +```csharp +HealthCheck = new HealthCheckConfig +{ + Active = new ActiveHealthCheckConfig + { + Enabled = true, + Interval = TimeSpan.FromSeconds(30), + Timeout = TimeSpan.FromSeconds(5), + Path = "/health" + } +} +``` + +**问题描述:** 健康检查路径和间隔硬编码,不同服务可能需要不同的健康检查配置。 + +**改进建议:** +- 将健康检查配置存储在数据库 +- 或在模型中添加健康检查配置字段 + +--- + +### 4.3 端口选择逻辑 + +**文件位置:** `src/Controllers/PendingServicesController.cs:119-120` +```csharp +var discoveredPorts = JsonSerializer.Deserialize>(pendingService.DiscoveredPorts) ?? new List(); +var primaryPort = discoveredPorts.FirstOrDefault() > 0 ? discoveredPorts.First() : 80; +``` + +**问题描述:** 简单选择第一个端口作为主端口,可能不适合所有服务场景。 + +**改进建议:** +- 支持端口选择策略配置 +- 优先选择知名端口(如 80, 443, 8080) +- 允许用户在审批时选择端口 + +--- + +### 4.4 异常处理不完整 + +**文件位置:** `src/Services/PgSqlConfigChangeListener.cs:59-62` +```csharp +catch (Exception ex) +{ + _logger.LogError(ex, "Failed to initialize PgSql listener"); + // 未重试或终止服务 +} +``` + +**问题描述:** 初始化失败后仅记录日志,服务继续运行但功能不完整。 + +**改进建议:** +```csharp +catch (Exception ex) +{ + _logger.LogError(ex, "Failed to initialize PgSql listener, retrying in 5 seconds..."); + await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken); + await InitializeListenerAsync(stoppingToken); // 重试 +} +``` + +--- + +### 4.5 状态变更无事务保护 + +**文件位置:** `src/Controllers/PendingServicesController.cs:137-145` +```csharp +db.ServiceInstances.Add(newInstance); +pendingService.Status = (int)PendingServiceStatus.Approved; +// ... +await db.SaveChangesAsync(); +``` + +**问题描述:** 创建实例和更新状态在同一事务中,但如果缓存重载失败,数据可能不一致。 + +**改进建议:** +- 使用 TransactionScope 或数据库事务明确边界 +- 添加补偿机制处理失败情况 + +--- + +## 五、可维护性问题 🟡 + +### 5.1 日志结构不统一 + +**问题描述:** 日志消息格式不统一,有的包含结构化数据,有的仅是文本。 + +**改进建议:** +- 制定统一的日志格式规范 +- 使用结构化日志模板:`LogInformation("Operation {Operation} completed for {Entity} with ID {Id}", "Create", "Route", route.Id)` + +--- + +### 5.2 缺少单元测试 + +**问题描述:** 项目中未发现测试项目,核心逻辑缺少测试覆盖。 + +**改进建议:** +- 创建 `tests/YarpGateway.Tests/` 测试项目 +- 对以下核心组件编写单元测试: + - `RouteCache` - 路由查找逻辑 + - `JwtTransformMiddleware` - JWT 解析逻辑 + - `DistributedWeightedRoundRobinPolicy` - 负载均衡算法 + +--- + +### 5.3 配置验证缺失 + +**文件位置:** `src/Config/JwtConfig.cs`, `src/Config/RedisConfig.cs` + +**问题描述:** 配置类没有验证逻辑,无效配置可能导致运行时错误。 + +**改进建议:** +```csharp +public class JwtConfig : IValidatableObject +{ + public string Authority { get; set; } = string.Empty; + + public IEnumerable Validate(ValidationContext validationContext) + { + if (string.IsNullOrWhiteSpace(Authority)) + yield return new ValidationResult("Authority is required", new[] { nameof(Authority) }); + } +} +``` + +--- + +## 六、改进优先级建议 + +| 优先级 | 问题 | 风险等级 | 建议处理时间 | +|--------|------|----------|--------------| +| P0 | 硬编码凭据泄露 | 严重 | 立即修复 | +| P0 | JWT未验证 | 严重 | 立即修复 | +| P0 | API无认证保护 | 严重 | 立即修复 | +| P1 | 租户ID信任问题 | 高 | 1周内 | +| P1 | ID生成策略 | 高 | 1周内 | +| P2 | 负载均衡锁竞争 | 中 | 2周内 | +| P2 | 路由缓存优化 | 中 | 2周内 | +| P3 | DTO内嵌定义 | 低 | 1个月内 | +| P3 | 缺少单元测试 | 低 | 持续改进 | + +--- + +## 七、总结 + +本项目存在多个**严重安全漏洞**,主要涉及: +1. 敏感信息硬编码 +2. 认证授权缺失 +3. 输入验证不足 + +技术债务主要集中在代码组织、异常处理和性能优化方面。建议优先处理安全相关问题,然后逐步优化性能和可维护性。 + +--- + +*文档由自动化分析生成,建议人工复核后纳入迭代计划。* \ No newline at end of file diff --git a/.planning/codebase/CONVENTIONS.md b/.planning/codebase/CONVENTIONS.md new file mode 100644 index 0000000..6661e0f --- /dev/null +++ b/.planning/codebase/CONVENTIONS.md @@ -0,0 +1,690 @@ +# YARP Gateway 编码约定文档 + +## 概述 + +本文档记录了 YARP Gateway 项目的编码约定和最佳实践,旨在帮助开发人员理解和遵循项目规范。 + +--- + +## 1. 代码风格 + +### 1.1 命名约定 + +#### 类和接口命名 + +```csharp +// 接口:使用 I 前缀 + PascalCase +public interface IRouteCache +{ + Task InitializeAsync(); + Task ReloadAsync(); + RouteInfo? GetRoute(string tenantCode, string serviceName); +} + +// 实现类:PascalCase,描述性名称 +public class RouteCache : IRouteCache +{ + // ... +} + +// 配置类:以 Config 后缀 +public class RedisConfig +{ + public string ConnectionString { get; set; } = "81.68.223.70:16379,password=sl52788542"; + public int Database { get; set; } = 0; + public string InstanceName { get; set; } = "YarpGateway"; +} + +// DTO 类:以 Dto 后缀 +public class CreateTenantDto +{ + public string TenantCode { get; set; } = string.Empty; + public string TenantName { get; set; } = string.Empty; +} + +// 数据模型:Gw 前缀标识网关实体 +public class GwTenantRoute +{ + public long Id { get; set; } + public string TenantCode { get; set; } = string.Empty; + // ... +} +``` + +#### 私有字段命名 + +```csharp +// 使用下划线前缀 + camelCase +public class TenantRoutingMiddleware +{ + private readonly RequestDelegate _next; + private readonly IRouteCache _routeCache; + private readonly ILogger _logger; +} +``` + +**原因**:下划线前缀清晰区分私有字段和局部变量,避免 `this.` 的频繁使用。 + +#### 方法命名 + +```csharp +// 异步方法:Async 后缀 +public async Task InitializeAsync() +public async Task ReloadAsync() +private async Task LoadFromDatabaseAsync() + +// 同步方法:动词开头 +public RouteInfo? GetRoute(string tenantCode, string serviceName) +private string ExtractServiceName(string path) +``` + +### 1.2 文件组织 + +项目采用按功能分层的方式组织代码: + +``` +src/ +├── Config/ # 配置类和配置提供者 +├── Controllers/ # API 控制器 +├── Data/ # 数据库上下文和工厂 +├── DynamicProxy/ # 动态代理配置 +├── LoadBalancing/ # 负载均衡策略 +├── Metrics/ # 指标收集 +├── Middleware/ # 中间件 +├── Migrations/ # 数据库迁移 +├── Models/ # 数据模型 +└── Services/ # 业务服务 +``` + +**原因**:按功能分层便于代码定位,降低耦合度。 + +--- + +## 2. 依赖注入模式 + +### 2.1 服务注册 + +```csharp +// Program.cs 中的服务注册 + +// 配置选项模式 +builder.Services.Configure(builder.Configuration.GetSection("Jwt")); +builder.Services.Configure(builder.Configuration.GetSection("Redis")); + +// 直接注册配置实例(当需要直接使用配置对象时) +builder.Services.AddSingleton(sp => sp.GetRequiredService>().Value); + +// DbContext 使用工厂模式 +builder.Services.AddDbContextFactory(options => + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")) +); + +// 单例服务(无状态或线程安全) +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +// 接口与实现分离注册 +builder.Services.AddSingleton(); + +// 后台服务 +builder.Services.AddHostedService(); +builder.Services.AddHostedService(); +``` + +### 2.2 依赖注入构造函数模式 + +```csharp +public class RouteCache : IRouteCache +{ + private readonly IDbContextFactory _dbContextFactory; + private readonly ILogger _logger; + + public RouteCache( + IDbContextFactory dbContextFactory, + ILogger logger) + { + _dbContextFactory = dbContextFactory; + _logger = logger; + } +} +``` + +**模式要点**: +1. 所有依赖通过构造函数注入 +2. 使用 `readonly` 修饰私有字段 +3. 依赖项按类别排序(框架 → 基础设施 → 业务服务) + +**原因**:构造函数注入确保依赖不可变,便于测试和依赖管理。 + +### 2.3 IDbContextFactory 模式 + +```csharp +// 在 Singleton 服务中使用 DbContextFactory +public class RouteCache : IRouteCache +{ + private readonly IDbContextFactory _dbContextFactory; + + private async Task LoadFromDatabaseAsync() + { + // 使用 using 确保上下文正确释放 + using var db = _dbContextFactory.CreateDbContext(); + var routes = await db.TenantRoutes + .Where(r => r.Status == 1 && !r.IsDeleted) + .ToListAsync(); + // ... + } +} + +// 在 BackgroundService 中使用 Scope +public class KubernetesPendingSyncService : BackgroundService +{ + private readonly IServiceProvider _serviceProvider; + + private async Task SyncPendingServicesAsync(CancellationToken ct) + { + // 创建作用域以获取 Scoped 服务 + using var scope = _serviceProvider.CreateScope(); + var dbContextFactory = scope.ServiceProvider.GetRequiredService>(); + // ... + } +} +``` + +**原因**:`IDbContextFactory` 避免了 Singleton 服务直接持有 DbContext 的生命周期问题。 + +--- + +## 3. 配置管理模式 + +### 3.1 配置类定义 + +```csharp +// 简单 POCO 配置类 +namespace YarpGateway.Config; + +public class JwtConfig +{ + public string Authority { get; set; } = string.Empty; + public string Audience { get; set; } = string.Empty; + public bool ValidateIssuer { get; set; } = true; + public bool ValidateAudience { get; set; } = true; +} +``` + +### 3.2 配置绑定和注入 + +```csharp +// Program.cs 中绑定配置 +builder.Services.Configure(builder.Configuration.GetSection("Jwt")); + +// 通过 IOptions 注入 +public class JwtTransformMiddleware +{ + private readonly JwtConfig _jwtConfig; + + public JwtTransformMiddleware( + RequestDelegate next, + IOptions jwtConfig, // 使用 IOptions + ILogger logger) + { + _jwtConfig = jwtConfig.Value; // 获取实际配置值 + _logger = logger; + } +} +``` + +### 3.3 动态配置更新 + +```csharp +// 配置变更通知通道 +public static class ConfigNotifyChannel +{ + public const string GatewayConfigChanged = "gateway_config_changed"; +} + +// DbContext 在保存时检测变更并通知 +public class GatewayDbContext : DbContext +{ + private bool _configChangeDetected; + + public override async Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) + { + DetectConfigChanges(); + var result = await base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); + if (_configChangeDetected) + { + await NotifyConfigChangedAsync(cancellationToken); + } + return result; + } + + private void DetectConfigChanges() + { + var entries = ChangeTracker.Entries() + .Where(e => e.State is EntityState.Added or EntityState.Modified or EntityState.Deleted) + .Where(e => e.Entity is GwTenantRoute or GwServiceInstance or GwTenant); + _configChangeDetected = entries.Any(); + } +} +``` + +**原因**:使用 PostgreSQL NOTIFY/LISTEN 实现配置热更新,避免轮询。 + +--- + +## 4. 错误处理方式 + +### 4.1 中间件错误处理 + +```csharp +public class JwtTransformMiddleware +{ + public async Task InvokeAsync(HttpContext context) + { + // 快速失败模式:前置条件检查后直接调用 next + var authHeader = context.Request.Headers["Authorization"].FirstOrDefault(); + if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer ")) + { + await _next(context); + return; + } + + try + { + // 业务逻辑 + var jwtHandler = new JwtSecurityTokenHandler(); + var jwtToken = jwtHandler.ReadJwtToken(token); + // ... + } + catch (Exception ex) + { + // 记录错误但不中断请求流程 + _logger.LogError(ex, "Failed to parse JWT token"); + } + + await _next(context); + } +} +``` + +### 4.2 控制器错误处理 + +```csharp +[HttpPost("{id}/assign")] +public async Task AssignService(long id, [FromBody] AssignServiceRequest request) +{ + await using var db = _dbContextFactory.CreateDbContext(); + + // 早期返回模式 + var pendingService = await db.PendingServiceDiscoveries.FindAsync(id); + if (pendingService == null || pendingService.IsDeleted) + { + return NotFound(new { message = "Pending service not found" }); + } + + if (pendingService.Status != (int)PendingServiceStatus.Pending) + { + return BadRequest(new { message = $"Service is already {((PendingServiceStatus)pendingService.Status)}, cannot assign" }); + } + + if (string.IsNullOrEmpty(request.ClusterId)) + { + return BadRequest(new { message = "ClusterId is required" }); + } + + // 业务逻辑... + return Ok(new { success = true, message = "..." }); +} +``` + +**模式要点**: +1. 使用早期返回(Guard Clauses)减少嵌套 +2. 返回结构化的错误信息 +3. 使用 HTTP 状态码语义 + +### 4.3 后台服务错误处理 + +```csharp +protected override async Task ExecuteAsync(CancellationToken stoppingToken) +{ + while (!stoppingToken.IsCancellationRequested) + { + try + { + await SyncPendingServicesAsync(stoppingToken); + } + catch (Exception ex) + { + // 记录错误但继续运行 + _logger.LogError(ex, "Error during K8s pending service sync"); + } + + await Task.Delay(_syncInterval, stoppingToken); + } +} +``` + +**原因**:后台服务不应因单次错误而终止,需具备自恢复能力。 + +--- + +## 5. 日志记录约定 + +### 5.1 结构化日志 + +```csharp +// 使用 Serilog 结构化日志 +_logger.LogInformation("Route cache initialized: {GlobalCount} global routes, {TenantCount} tenant routes", + _globalRoutes.Count, _tenantRoutes.Count); + +_logger.LogWarning("No route found for: {Tenant}/{Service}", tenantCode, serviceName); + +_logger.LogError(ex, "Redis connection failed"); + +_logger.LogDebug("Released lock for key: {Key}", _key); +``` + +**模式要点**: +1. 使用占位符 `{PropertyName}` 而非字符串插值 +2. 日志消息使用常量,便于聚合分析 +3. 包含足够的上下文信息 + +### 5.2 Serilog 配置 + +```csharp +// Program.cs +builder.Host.UseSerilog( + (context, services, configuration) => + configuration + .ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() +); +``` + +### 5.3 日志级别使用 + +| 级别 | 使用场景 | +|------|----------| +| `LogDebug` | 详细调试信息,生产环境通常关闭 | +| `LogInformation` | 正常业务流程关键节点 | +| `LogWarning` | 可恢复的异常情况 | +| `LogError` | 错误需要关注但不影响整体运行 | +| `LogFatal` | 致命错误,应用无法继续运行 | + +--- + +## 6. 异步编程模式 + +### 6.1 async/await 使用 + +```csharp +// 正确:异步方法使用 Async 后缀 +public async Task InitializeAsync() +{ + _logger.LogInformation("Initializing route cache from database..."); + await LoadFromDatabaseAsync(); +} + +// 正确:使用 ConfigureAwait(false) 在库代码中 +private async Task LoadFromDatabaseAsync() +{ + using var db = _dbContextFactory.CreateDbContext(); + var routes = await db.TenantRoutes + .Where(r => r.Status == 1 && !r.IsDeleted) + .ToListAsync(); + // ... +} +``` + +### 6.2 CancellationToken 使用 + +```csharp +// 控制器方法 +[HttpGet] +public async Task GetPendingServices( + [FromQuery] int page = 1, + [FromQuery] int pageSize = 10, + [FromQuery] int? status = null) +{ + await using var db = _dbContextFactory.CreateDbContext(); + // EF Core 自动处理 CancellationToken + var total = await query.CountAsync(); + // ... +} + +// 后台服务 +protected override async Task ExecuteAsync(CancellationToken stoppingToken) +{ + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(_syncInterval, stoppingToken); + } +} +``` + +### 6.3 并发控制 + +```csharp +// 使用 ReaderWriterLockSlim 保护读写 +public class RouteCache : IRouteCache +{ + private readonly ReaderWriterLockSlim _lock = new(); + + public RouteInfo? GetRoute(string tenantCode, string serviceName) + { + _lock.EnterUpgradeableReadLock(); + try + { + // 读取逻辑 + } + finally + { + _lock.ExitUpgradeableReadLock(); + } + } + + private async Task LoadFromDatabaseAsync() + { + _lock.EnterWriteLock(); + try + { + // 写入逻辑 + } + finally + { + _lock.ExitWriteLock(); + } + } +} + +// 使用 SemaphoreSlim 进行异步锁定 +public class DatabaseRouteConfigProvider +{ + private readonly SemaphoreSlim _lock = new(1, 1); + + public async Task ReloadAsync() + { + await _lock.WaitAsync(); + try + { + await LoadConfigInternalAsync(); + } + finally + { + _lock.Release(); + } + } +} +``` + +**原因**: +- `ReaderWriterLockSlim` 支持多读单写,适合读多写少场景 +- `SemaphoreSlim` 支持异步等待,适合异步方法 + +### 6.4 Redis 分布式锁模式 + +```csharp +public async Task AcquireLockAsync(string key, TimeSpan? expiry = null) +{ + var redis = GetConnection(); + var db = redis.GetDatabase(); + var lockKey = $"lock:{_config.InstanceName}:{key}"; + var lockValue = Environment.MachineName + ":" + Process.GetCurrentProcess().Id; + + var acquired = await db.StringSetAsync(lockKey, lockValue, expiryTime, When.NotExists); + + if (!acquired) + { + // 退避重试 + var backoff = TimeSpan.FromMilliseconds(100); + while (!acquired && retryCount < maxRetries) + { + await Task.Delay(backoff); + acquired = await db.StringSetAsync(lockKey, lockValue, expiryTime, When.NotExists); + retryCount++; + } + } + + return new RedisLock(db, lockKey, lockValue, _logger); +} +``` + +--- + +## 7. 中间件模式 + +### 7.1 标准中间件结构 + +```csharp +public class TenantRoutingMiddleware +{ + private readonly RequestDelegate _next; + private readonly IRouteCache _routeCache; + private readonly ILogger _logger; + + // 构造函数注入依赖 + public TenantRoutingMiddleware( + RequestDelegate next, + IRouteCache routeCache, + ILogger logger) + { + _next = next; + _routeCache = routeCache; + _logger = logger; + } + + // InvokeAsync 方法签名固定 + public async Task InvokeAsync(HttpContext context) + { + // 1. 前置处理 + var tenantId = context.Request.Headers["X-Tenant-Id"].FirstOrDefault(); + + // 2. 快速返回 + if (string.IsNullOrEmpty(tenantId)) + { + await _next(context); + return; + } + + // 3. 业务逻辑 + var route = _routeCache.GetRoute(tenantId, serviceName); + + // 4. 设置上下文 + context.Items["DynamicClusterId"] = route.ClusterId; + + // 5. 调用下一个中间件 + await _next(context); + } +} +``` + +### 7.2 中间件注册顺序 + +```csharp +// Program.cs +var app = builder.Build(); + +app.UseCors("AllowFrontend"); +app.UseMiddleware(); // JWT 解析 +app.UseMiddleware(); // 租户路由 + +app.MapControllers(); +app.MapReverseProxy(); +``` + +**顺序原因**: +1. CORS 需最先处理跨域请求 +2. JWT 中间件解析用户信息供后续使用 +3. 租户路由根据用户信息选择目标服务 + +--- + +## 8. 控制器约定 + +### 8.1 控制器结构 + +```csharp +[ApiController] +[Route("api/gateway")] +public class GatewayConfigController : ControllerBase +{ + private readonly IDbContextFactory _dbContextFactory; + private readonly IRouteCache _routeCache; + + public GatewayConfigController( + IDbContextFactory dbContextFactory, + IRouteCache routeCache) + { + _dbContextFactory = dbContextFactory; + _routeCache = routeCache; + } + + #region Tenants + // 租户相关端点 + #endregion + + #region Routes + // 路由相关端点 + #endregion +} +``` + +### 8.2 端点命名 + +```csharp +// GET 集合 +[HttpGet("tenants")] +public async Task GetTenants(...) { } + +// GET 单个 +[HttpGet("tenants/{id}")] +public async Task GetTenant(long id) { } + +// POST 创建 +[HttpPost("tenants")] +public async Task CreateTenant([FromBody] CreateTenantDto dto) { } + +// PUT 更新 +[HttpPut("tenants/{id}")] +public async Task UpdateTenant(long id, [FromBody] UpdateTenantDto dto) { } + +// DELETE 删除 +[HttpDelete("tenants/{id}")] +public async Task DeleteTenant(long id) { } +``` + +--- + +## 9. 总结 + +本项目的编码约定遵循以下核心原则: + +1. **一致性**:统一的命名和代码组织方式 +2. **可测试性**:依赖注入和接口抽象便于测试 +3. **可维护性**:清晰的结构和文档注释 +4. **可观测性**:结构化日志和指标收集 +5. **健壮性**:完善的错误处理和并发控制 + +遵循这些约定可以确保代码质量和团队协作效率。 \ No newline at end of file diff --git a/.planning/codebase/INTEGRATIONS.md b/.planning/codebase/INTEGRATIONS.md new file mode 100644 index 0000000..0d6d558 --- /dev/null +++ b/.planning/codebase/INTEGRATIONS.md @@ -0,0 +1,374 @@ +# YARP 网关外部集成文档 + +## 1. PostgreSQL 数据库集成 + +### 概述 +PostgreSQL 作为主数据库,存储网关配置数据,包括租户、路由、服务实例等信息。 + +### 连接配置 +**配置位置**: `src/appsettings.json` + +```json +{ + "ConnectionStrings": { + "DefaultConnection": "Host=81.68.223.70;Port=15432;Database=fengling_gateway;Username=movingsam;Password=***" + } +} +``` + +### DbContext 配置 +**文件**: `src/Data/GatewayDbContext.cs` + +```csharp +// 注册 DbContext 工厂 +builder.Services.AddDbContextFactory(options => + options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection")) +); +``` + +### 数据模型 +| 实体 | 表名 | 用途 | +|------|------|------| +| `GwTenant` | Tenants | 租户信息 | +| `GwTenantRoute` | TenantRoutes | 租户路由配置 | +| `GwServiceInstance` | ServiceInstances | 服务实例(集群节点) | +| `GwPendingServiceDiscovery` | PendingServiceDiscoveries | K8s 待处理服务发现 | + +### 配置变更通知机制 +**文件**: `src/Config/ConfigNotifyChannel.cs` + +使用 PostgreSQL `LISTEN/NOTIFY` 机制实现配置变更实时通知: + +```csharp +// 发送通知(在 DbContext.SaveChangesAsync 中触发) +await using var cmd = new NpgsqlCommand($"NOTIFY {ConfigNotifyChannel.GatewayConfigChanged}", connection); + +// 监听通知(在 PgSqlConfigChangeListener 中) +cmd.CommandText = $"LISTEN {ConfigNotifyChannel.GatewayConfigChanged}"; +``` + +**监听服务**: `src/Services/PgSqlConfigChangeListener.cs` +- 监听 PostgreSQL NOTIFY 通道 +- 检测配置版本变更 +- 触发路由/集群配置热更新 +- 提供 5 分钟兜底轮询机制 + +--- + +## 2. Redis 集成 + +### 概述 +Redis 用于分布式锁、路由缓存同步,确保多实例网关的配置一致性。 + +### 连接配置 +**配置位置**: `src/Config/RedisConfig.cs` + +```csharp +public class RedisConfig +{ + public string ConnectionString { get; set; } = "81.68.223.70:16379,password=***"; + public int Database { get; set; } = 0; + public string InstanceName { get; set; } = "YarpGateway"; +} +``` + +### 连接管理器 +**文件**: `src/Services/RedisConnectionManager.cs` + +```csharp +// 注册 Redis 连接 +builder.Services.AddSingleton(sp => +{ + var config = sp.GetRequiredService(); + var connectionOptions = ConfigurationOptions.Parse(config.ConnectionString); + connectionOptions.AbortOnConnectFail = false; + connectionOptions.ConnectRetry = 3; + connectionOptions.ConnectTimeout = 5000; + connectionOptions.SyncTimeout = 3000; + connectionOptions.DefaultDatabase = config.Database; + + return ConnectionMultiplexer.Connect(connectionOptions); +}); +``` + +### 分布式锁实现 +**接口**: `IRedisConnectionManager` + +```csharp +public interface IRedisConnectionManager +{ + IConnectionMultiplexer GetConnection(); + Task AcquireLockAsync(string key, TimeSpan? expiry = null); + Task ExecuteInLockAsync(string key, Func> func, TimeSpan? expiry = null); +} +``` + +**锁机制特性**: +- 基于键值对的分布式锁 +- 自动过期时间(默认 10 秒) +- 指数退避重试策略 +- Lua 脚本安全释放锁 + +--- + +## 3. Kubernetes 服务发现集成 + +### 概述 +通过自定义的 Fengling.ServiceDiscovery 包实现 Kubernetes 服务自动发现,将 K8s Service 自动注册为网关后端服务。 + +### 配置 +**文件**: `src/Program.cs` + +```csharp +// 添加 Kubernetes 服务发现 +var useInClusterConfig = builder.Configuration.GetValue("ServiceDiscovery:UseInClusterConfig", true); +builder.Services.AddKubernetesServiceDiscovery(options => +{ + options.LabelSelector = "app.kubernetes.io/managed-by=yarp"; + options.UseInClusterConfig = useInClusterConfig; +}); + +builder.Services.AddServiceDiscovery(); +``` + +### 依赖包 +| 包名 | 用途 | +|------|------| +| `Fengling.ServiceDiscovery.Core` | 服务发现核心接口 | +| `Fengling.ServiceDiscovery.Kubernetes` | Kubernetes 实现 | +| `Fengling.ServiceDiscovery.Static` | 静态配置实现 | + +### 后台同步服务 +**文件**: `src/Services/KubernetesPendingSyncService.cs` + +```csharp +public class KubernetesPendingSyncService : BackgroundService +{ + private readonly TimeSpan _syncInterval = TimeSpan.FromSeconds(30); + private readonly TimeSpan _staleThreshold = TimeSpan.FromHours(24); + + // 同步 K8s 服务到数据库待处理表 +} +``` + +**同步逻辑**: +1. 每 30 秒从 K8s API 获取服务列表 +2. 对比数据库中的待处理服务记录 +3. 新增/更新/清理过期服务 +4. 标记不再存在的 K8s 服务 + +### 待处理服务数据模型 +**文件**: `src/Models/GwPendingServiceDiscovery.cs` + +```csharp +public class GwPendingServiceDiscovery +{ + public string K8sServiceName { get; set; } // K8s Service 名称 + public string K8sNamespace { get; set; } // K8s 命名空间 + public string K8sClusterIP { get; set; } // ClusterIP + public string DiscoveredPorts { get; set; } // JSON 序列化的端口列表 + public string Labels { get; set; } // K8s 标签 + public string AssignedClusterId { get; set; } // 分配的集群 ID + public int Status { get; set; } // 状态 +} +``` + +--- + +## 4. JWT 认证集成 + +### 概述 +网关解析 JWT Token,提取租户和用户信息,转换为下游服务可用的 HTTP 头。 + +### 配置 +**文件**: `src/Config/JwtConfig.cs` + +```csharp +public class JwtConfig +{ + public string Authority { get; set; } = string.Empty; // 认证服务器地址 + public string Audience { get; set; } = string.Empty; // 受众 + public bool ValidateIssuer { get; set; } = true; // 验证签发者 + public bool ValidateAudience { get; set; } = true; // 验证受众 +} +``` + +**配置示例** (`src/appsettings.json`): +```json +{ + "Jwt": { + "Authority": "https://your-auth-server.com", + "Audience": "fengling-gateway", + "ValidateIssuer": true, + "ValidateAudience": true + } +} +``` + +### JWT 转换中间件 +**文件**: `src/Middleware/JwtTransformMiddleware.cs` + +```csharp +public async Task InvokeAsync(HttpContext context) +{ + var authHeader = context.Request.Headers["Authorization"].FirstOrDefault(); + if (!string.IsNullOrEmpty(authHeader) && authHeader.StartsWith("Bearer ")) + { + var token = authHeader.Substring("Bearer ".Length).Trim(); + var jwtToken = jwtHandler.ReadJwtToken(token); + + // 提取声明并转换为 HTTP 头 + var tenantId = jwtToken.Claims.FirstOrDefault(c => c.Type == "tenant")?.Value; + var userId = jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; + + context.Request.Headers["X-Tenant-Id"] = tenantId; + context.Request.Headers["X-User-Id"] = userId; + context.Request.Headers["X-User-Name"] = userName; + context.Request.Headers["X-Roles"] = string.Join(",", roles); + } + await _next(context); +} +``` + +### JWT 声明到 HTTP 头映射 +| JWT 声明类型 | HTTP 头 | 说明 | +|--------------|---------|------| +| `tenant` | `X-Tenant-Id` | 租户标识 | +| `ClaimTypes.NameIdentifier` | `X-User-Id` | 用户 ID | +| `ClaimTypes.Name` | `X-User-Name` | 用户名 | +| `ClaimTypes.Role` | `X-Roles` | 角色列表(逗号分隔) | + +--- + +## 5. 外部 API 和服务连接 + +### CORS 配置 +**文件**: `src/appsettings.json` + +```json +{ + "Cors": { + "AllowedOrigins": [ + "http://localhost:5173", + "http://127.0.0.1:5173", + "http://localhost:5174" + ], + "AllowAnyOrigin": false + } +} +``` + +### 健康检查端点 +**文件**: `src/Program.cs` + +```csharp +app.MapGet("/health", () => Results.Ok(new { + status = "healthy", + timestamp = DateTime.UtcNow +})); +``` + +### 下游服务健康检查 +**文件**: `src/Config/DatabaseClusterConfigProvider.cs` + +```csharp +HealthCheck = new HealthCheckConfig +{ + Active = new ActiveHealthCheckConfig + { + Enabled = true, + Interval = TimeSpan.FromSeconds(30), + Timeout = TimeSpan.FromSeconds(5), + Path = "/health" + } +} +``` + +### 动态代理配置 +**文件**: `src/DynamicProxy/DynamicProxyConfigProvider.cs` + +实现 `IProxyConfigProvider` 接口,从数据库动态加载路由和集群配置: + +```csharp +public class DynamicProxyConfigProvider : IProxyConfigProvider +{ + public IProxyConfig GetConfig() => _config; + + public void UpdateConfig() + { + var routes = _routeProvider.GetRoutes(); + var clusters = _clusterProvider.GetClusters(); + _config = new InMemoryProxyConfig(routes, clusters, ...); + } +} +``` + +--- + +## 6. 集成架构图 + +``` + ┌─────────────────────────────────────────────────────────────┐ + │ 客户端请求 │ + └─────────────────────────────────────────────────────────────┘ + │ + ▼ + ┌─────────────────────────────────────────────────────────────┐ + │ YARP Gateway │ + │ ┌─────────────────────────────────────────────────────┐ │ + │ │ 中间件管道 │ │ + │ │ CORS → JWT转换 → 租户路由 → Controllers → Proxy │ │ + │ └─────────────────────────────────────────────────────┘ │ + │ │ + │ ┌──────────┐ ┌──────────┐ ┌──────────────────────────┐ │ + │ │RouteCache│ │ConfigProv│ │LoadBalancingPolicy │ │ + │ └────┬─────┘ └────┬─────┘ └──────────────────────────┘ │ + └───────┼─────────────┼────────────────────────────────────────┘ + │ │ + ┌───────────────┼─────────────┼───────────────┐ + │ │ │ │ + ▼ ▼ ▼ ▼ + ┌───────────────┐ ┌───────────┐ ┌───────────┐ ┌───────────────────┐ + │ PostgreSQL │ │ Redis │ │ K8s │ │ Auth Server │ + │ │ │ │ │ API │ │ (JWT) │ + │ - 租户配置 │ │ - 分布式锁│ │ │ │ │ + │ - 路由配置 │ │ - 缓存 │ │ - Service │ │ - Token 签发 │ + │ - 服务实例 │ │ │ │ - Pod │ │ - 声明信息 │ + │ - NOTIFY机制 │ │ │ │ │ │ │ + └───────────────┘ └───────────┘ └───────────┘ └───────────────────┘ + │ + │ LISTEN/NOTIFY + ▼ + ┌───────────────────────────────────────────────────────┐ + │ 配置变更监听器 │ + │ PgSqlConfigChangeListener + FallbackPolling │ + └───────────────────────────────────────────────────────┘ +``` + +--- + +## 7. 配置热更新流程 + +``` +数据库配置变更 + │ + ▼ +DbContext.SaveChangesAsync() + │ + ▼ +NOTIFY gateway_config_changed + │ + ▼ +PgSqlConfigChangeListener.OnNotification() + │ + ▼ +RouteCache.ReloadAsync() + │ + ▼ +DynamicProxyConfigProvider.UpdateConfig() + │ + ▼ +YARP 配置生效(无需重启) +``` + +**兜底机制**: 每 5 分钟检查版本号,防止 NOTIFY 丢失导致配置不一致。 \ No newline at end of file diff --git a/.planning/codebase/STACK.md b/.planning/codebase/STACK.md new file mode 100644 index 0000000..4db5082 --- /dev/null +++ b/.planning/codebase/STACK.md @@ -0,0 +1,189 @@ +# YARP 网关技术栈文档 + +## 1. 语言和运行时 + +### .NET 版本 +- **目标框架**: .NET 10.0 +- **项目文件**: `src/YarpGateway.csproj` +- **SDK**: `Microsoft.NET.Sdk.Web` + +```xml +net10.0 +enable +enable +``` + +## 2. 核心框架 + +### YARP (Yet Another Reverse Proxy) +- **包**: `Yarp.ReverseProxy` +- **用途**: 微服务 API 网关核心反向代理引擎 +- **主要功能**: + - 动态路由配置 + - 负载均衡策略 + - 健康检查 + - 请求转发 + +### ASP.NET Core +- **用途**: Web 应用宿主框架 +- **特性**: + - 依赖注入 (DI) + - 中间件管道 + - 配置系统 + - 日志集成 + +## 3. 主要依赖包 + +### 数据访问 +| 包名 | 用途 | +|------|------| +| `Npgsql.EntityFrameworkCore.PostgreSQL` | PostgreSQL Entity Framework Core 提供程序 | +| `Microsoft.EntityFrameworkCore.Design` | EF Core 设计时工具(迁移) | + +### 缓存与分布式锁 +| 包名 | 用途 | +|------|------| +| `StackExchange.Redis` | Redis 客户端,用于分布式锁和缓存 | + +### 认证授权 +| 包名 | 用途 | +|------|------| +| `Microsoft.AspNetCore.Authentication.JwtBearer` | JWT Bearer 认证支持 | + +### 日志 +| 包名 | 用途 | +|------|------| +| `Serilog.AspNetCore` | Serilog ASP.NET Core 集成 | +| `Serilog.Sinks.Console` | 控制台日志输出 | +| `Serilog.Sinks.File` | 文件日志输出 | + +### 服务发现(自定义包) +| 包名 | 用途 | +|------|------| +| `Fengling.ServiceDiscovery.Core` | 服务发现核心接口 | +| `Fengling.ServiceDiscovery.Kubernetes` | Kubernetes 服务发现实现 | +| `Fengling.ServiceDiscovery.Static` | 静态配置服务发现 | + +## 4. 配置文件 + +### 主配置文件 +**位置**: `src/appsettings.json` + +```json +{ + "ConnectionStrings": { + "DefaultConnection": "Host=...;Port=...;Database=...;Username=...;Password=..." + }, + "Jwt": { + "Authority": "https://your-auth-server.com", + "Audience": "fengling-gateway", + "ValidateIssuer": true, + "ValidateAudience": true + }, + "Redis": { + "ConnectionString": "host:port", + "Database": 0, + "InstanceName": "YarpGateway" + }, + "Cors": { + "AllowedOrigins": ["http://localhost:5173"], + "AllowAnyOrigin": false + }, + "Kestrel": { + "Endpoints": { + "Http": { "Url": "http://0.0.0.0:8080" } + } + }, + "Serilog": { + "MinimumLevel": "Information", + "WriteTo": [ + { "Name": "Console" }, + { "Name": "File", "Args": { "path": "logs/gateway-.log", "rollingInterval": "Day" } } + ] + } +} +``` + +### 配置类 +| 文件路径 | 类名 | 用途 | +|----------|------|------| +| `src/Config/JwtConfig.cs` | `JwtConfig` | JWT 认证配置 | +| `src/Config/RedisConfig.cs` | `RedisConfig` | Redis 连接配置 | +| `src/Config/ConfigNotifyChannel.cs` | `ConfigNotifyChannel` | PostgreSQL NOTIFY 通道常量 | + +## 5. Docker 支持 + +### Dockerfile +**位置**: `Dockerfile` + +```dockerfile +# 基础镜像 +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS base +EXPOSE 8080 +EXPOSE 8081 + +# 构建镜像 +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build +# 多阶段构建... + +# 最终镜像 +FROM base AS final +ENTRYPOINT ["dotnet", "YarpGateway.dll"] +``` + +### Docker 配置 +- **默认目标 OS**: Linux +- **暴露端口**: 8080 (HTTP), 8081 (HTTPS) +- **工作目录**: `/app` + +## 6. 项目结构 + +``` +src/ +├── Config/ # 配置类 +│ ├── JwtConfig.cs +│ ├── RedisConfig.cs +│ ├── ConfigNotifyChannel.cs +│ ├── DatabaseRouteConfigProvider.cs +│ └── DatabaseClusterConfigProvider.cs +├── Data/ # 数据访问层 +│ ├── GatewayDbContext.cs +│ └── GatewayDbContextFactory.cs +├── DynamicProxy/ # 动态代理配置 +│ └── DynamicProxyConfigProvider.cs +├── LoadBalancing/ # 负载均衡策略 +│ └── DistributedWeightedRoundRobinPolicy.cs +├── Middleware/ # 中间件 +│ ├── JwtTransformMiddleware.cs +│ └── TenantRoutingMiddleware.cs +├── Models/ # 数据模型 +│ ├── GwTenant.cs +│ ├── GwTenantRoute.cs +│ ├── GwServiceInstance.cs +│ └── GwPendingServiceDiscovery.cs +├── Services/ # 业务服务 +│ ├── RouteCache.cs +│ ├── RedisConnectionManager.cs +│ ├── KubernetesPendingSyncService.cs +│ └── PgSqlConfigChangeListener.cs +├── Program.cs # 应用入口 +├── appsettings.json # 配置文件 +└── YarpGateway.csproj # 项目文件 +``` + +## 7. 中间件管道 + +请求处理管道顺序(`Program.cs`): + +1. **CORS** - 跨域请求处理 +2. **JwtTransformMiddleware** - JWT 解析与转换 +3. **TenantRoutingMiddleware** - 租户路由解析 +4. **Controllers** - API 控制器 +5. **ReverseProxy** - YARP 反向代理 + +## 8. 托管与部署 + +### Kestrel 配置 +- 监听地址: `http://0.0.0.0:8080` +- 支持 Docker 容器化部署 +- 支持 Kubernetes 集群部署 \ No newline at end of file diff --git a/.planning/codebase/STRUCTURE.md b/.planning/codebase/STRUCTURE.md new file mode 100644 index 0000000..8b9cdba --- /dev/null +++ b/.planning/codebase/STRUCTURE.md @@ -0,0 +1,465 @@ +# YARP Gateway 目录结构文档 + +## 1. 目录布局 + +``` +fengling-gateway/ +├── .planning/ # 规划文档目录 +│ └── codebase/ # 代码库分析文档 +│ ├── ARCHITECTURE.md # 架构文档 +│ └── STRUCTURE.md # 本文档 +│ +├── src/ # 源代码目录 +│ ├── Config/ # 配置类和提供者 +│ ├── Controllers/ # API 控制器 +│ ├── Data/ # 数据访问层 +│ ├── DynamicProxy/ # YARP 动态代理 +│ ├── LoadBalancing/ # 负载均衡策略 +│ ├── Migrations/ # 数据库迁移 +│ ├── Metrics/ # 监控指标 +│ ├── Middleware/ # 中间件 +│ ├── Models/ # 数据模型 +│ ├── Properties/ # 项目属性 +│ ├── Services/ # 业务服务 +│ ├── Program.cs # 程序入口 +│ ├── YarpGateway.csproj # 项目文件 +│ ├── appsettings.json # 配置文件 +│ └── appsettings.Development.json # 开发环境配置 +│ +└── (根目录其他文件) +``` + +--- + +## 2. 详细目录说明 + +### 2.1 Config/ - 配置层 + +**路径**: `src/Config/` + +**用途**: 存放配置模型和配置提供者 + +| 文件 | 行数 | 用途 | +|------|------|------| +| `JwtConfig.cs` | 10 | JWT 认证配置模型,包含 Authority、Audience 等属性 | +| `RedisConfig.cs` | 9 | Redis 连接配置模型,包含连接字符串、数据库索引等 | +| `ConfigNotifyChannel.cs` | 7 | PostgreSQL NOTIFY 通道名称常量定义 | +| `DatabaseRouteConfigProvider.cs` | 84 | 从数据库加载路由配置,转换为 YARP RouteConfig | +| `DatabaseClusterConfigProvider.cs` | 100 | 从数据库加载集群配置,管理服务实例列表 | + +**设计特点**: +- 配置类使用 POCO 模型,通过 Options 模式注入 +- Provider 类使用单例模式,支持热重载 + +--- + +### 2.2 Controllers/ - 控制器层 + +**路径**: `src/Controllers/` + +**用途**: RESTful API 端点 + +| 文件 | 行数 | 路由前缀 | 用途 | +|------|------|----------|------| +| `GatewayConfigController.cs` | 489 | `/api/gateway` | 网关配置管理 API | +| `PendingServicesController.cs` | 210 | `/api/gateway/pending-services` | 待处理服务管理 API | + +**GatewayConfigController 端点**: + +| 方法 | 路由 | 功能 | +|------|------|------| +| GET | `/tenants` | 获取租户列表(分页) | +| GET | `/tenants/{id}` | 获取单个租户 | +| POST | `/tenants` | 创建租户 | +| PUT | `/tenants/{id}` | 更新租户 | +| DELETE | `/tenants/{id}` | 删除租户 | +| GET | `/routes` | 获取路由列表(分页) | +| GET | `/routes/global` | 获取全局路由 | +| GET | `/routes/tenant/{tenantCode}` | 获取租户路由 | +| POST | `/routes` | 创建路由 | +| PUT | `/routes/{id}` | 更新路由 | +| DELETE | `/routes/{id}` | 删除路由 | +| GET | `/clusters` | 获取集群列表 | +| GET | `/clusters/{clusterId}` | 获取集群详情 | +| POST | `/clusters` | 创建集群 | +| DELETE | `/clusters/{clusterId}` | 删除集群 | +| GET | `/clusters/{clusterId}/instances` | 获取实例列表 | +| POST | `/clusters/{clusterId}/instances` | 添加实例 | +| DELETE | `/instances/{id}` | 删除实例 | +| POST | `/config/reload` | 重载配置 | +| GET | `/config/status` | 获取配置状态 | +| GET | `/config/versions` | 获取版本信息 | +| GET | `/stats/overview` | 获取统计概览 | + +**PendingServicesController 端点**: + +| 方法 | 路由 | 功能 | +|------|------|------| +| GET | `/` | 获取待处理服务列表 | +| GET | `/{id}` | 获取待处理服务详情 | +| POST | `/{id}/assign` | 分配服务到集群 | +| POST | `/{id}/reject` | 拒绝服务 | +| GET | `/clusters` | 获取可用集群列表 | + +--- + +### 2.3 Data/ - 数据访问层 + +**路径**: `src/Data/` + +**用途**: Entity Framework Core 数据库上下文 + +| 文件 | 行数 | 用途 | +|------|------|------| +| `GatewayDbContext.cs` | 142 | EF Core 数据库上下文,包含实体配置和变更通知 | +| `GatewayDbContextFactory.cs` | 23 | 设计时 DbContext 工厂,用于迁移命令 | + +**DbContext 特性**: +- 自动检测配置变更 +- 集成 PostgreSQL NOTIFY 机制 +- 支持软删除(IsDeleted 标记) +- 版本号追踪(Version 字段) + +--- + +### 2.4 DynamicProxy/ - 动态代理层 + +**路径**: `src/DynamicProxy/` + +**用途**: YARP 动态配置提供 + +| 文件 | 行数 | 用途 | +|------|------|------| +| `DynamicProxyConfigProvider.cs` | 79 | 实现 IProxyConfigProvider,整合路由和集群配置 | + +**核心职责**: +- 实现 YARP 配置提供接口 +- 协调 Route 和 Cluster 配置 +- 提供配置变更通知(通过 CancellationToken) + +--- + +### 2.5 LoadBalancing/ - 负载均衡层 + +**路径**: `src/LoadBalancing/` + +**用途**: 自定义负载均衡策略 + +| 文件 | 行数 | 用途 | +|------|------|------| +| `DistributedWeightedRoundRobinPolicy.cs` | 244 | 基于 Redis 的分布式加权轮询策略 | + +**策略特点**: +- 策略名称: `DistributedWeightedRoundRobin` +- 支持实例权重配置 +- Redis 分布式状态存储 +- 降级策略(锁获取失败时) + +--- + +### 2.6 Migrations/ - 数据库迁移 + +**路径**: `src/Migrations/` + +**用途**: Entity Framework Core 迁移文件 + +| 文件 | 用途 | +|------|------| +| `20260201120312_InitialCreate.cs` | 初始数据库创建 | +| `20260201133826_AddIsGlobalToTenantRoute.cs` | 添加 IsGlobal 字段 | +| `20260222134342_AddPendingServiceDiscovery.cs` | 添加待处理服务发现表 | +| `*ModelSnapshot.cs` | 当前模型快照 | +| `*.Designer.cs` | 设计器生成文件 | + +--- + +### 2.7 Metrics/ - 监控指标 + +**路径**: `src/Metrics/` + +**用途**: OpenTelemetry 指标定义 + +| 文件 | 行数 | 用途 | +|------|------|------| +| `GatewayMetrics.cs` | 31 | 定义网关监控指标 | + +**指标列表**: +- `gateway_requests_total` - 请求总数计数器 +- `gateway_request_duration_seconds` - 请求延迟直方图 + +--- + +### 2.8 Middleware/ - 中间件层 + +**路径**: `src/Middleware/` + +**用途**: ASP.NET Core 中间件 + +| 文件 | 行数 | 用途 | +|------|------|------| +| `JwtTransformMiddleware.cs` | 84 | JWT Token 解析,提取租户信息注入请求头 | +| `TenantRoutingMiddleware.cs` | 64 | 租户路由解析,根据路径查找目标集群 | + +**中间件执行顺序**: +``` +CORS -> JwtTransformMiddleware -> TenantRoutingMiddleware -> YARP +``` + +--- + +### 2.9 Models/ - 数据模型层 + +**路径**: `src/Models/` + +**用途**: 实体类定义 + +| 文件 | 行数 | 用途 | +|------|------|------| +| `GwTenant.cs` | 16 | 租户实体 | +| `GwTenantRoute.cs` | 20 | 路由配置实体 | +| `GwServiceInstance.cs` | 19 | 服务实例实体 | +| `GwPendingServiceDiscovery.cs` | 28 | 待处理服务发现实体 + 状态枚举 | + +**实体通用字段**: +- `Id` - 主键(雪花 ID 格式) +- `Status` - 状态(1=启用) +- `CreatedBy/UpdatedBy` - 操作人 +- `CreatedTime/UpdatedTime` - 时间戳 +- `IsDeleted` - 软删除标记 +- `Version` - 版本号(乐观锁) + +--- + +### 2.10 Services/ - 服务层 + +**路径**: `src/Services/` + +**用途**: 业务逻辑和后台服务 + +| 文件 | 行数 | 类型 | 用途 | +|------|------|------|------| +| `RouteCache.cs` | 139 | Singleton | 路由缓存,支持租户路由和全局路由 | +| `RedisConnectionManager.cs` | 139 | Singleton | Redis 连接管理,分布式锁实现 | +| `PgSqlConfigChangeListener.cs` | 223 | HostedService | PostgreSQL 配置变更监听 | +| `KubernetesPendingSyncService.cs` | 162 | HostedService | Kubernetes 服务发现同步 | + +**服务生命周期**: +- Singleton: RouteCache, RedisConnectionManager(状态服务) +- HostedService: PgSqlConfigChangeListener, KubernetesPendingSyncService(后台任务) + +--- + +## 3. 关键文件位置 + +### 3.1 入口文件 + +| 文件 | 路径 | 用途 | +|------|------|------| +| `Program.cs` | `src/Program.cs` | 应用程序入口,服务注册和中间件配置 | + +### 3.2 配置文件 + +| 文件 | 路径 | 用途 | +|------|------|------| +| `appsettings.json` | `src/appsettings.json` | 生产环境配置 | +| `appsettings.Development.json` | `src/appsettings.Development.json` | 开发环境配置 | +| `YarpGateway.csproj` | `src/YarpGateway.csproj` | 项目文件,包引用 | + +### 3.3 数据库相关 + +| 文件 | 路径 | 用途 | +|------|------|------| +| `GatewayDbContext.cs` | `src/Data/GatewayDbContext.cs` | 数据库上下文 | +| `GatewayDbContextFactory.cs` | `src/Data/GatewayDbContextFactory.cs` | 迁移工具工厂 | + +--- + +## 4. 命名约定 + +### 4.1 文件命名 + +| 类型 | 命名规则 | 示例 | +|------|----------|------| +| 实体类 | `Gw` 前缀 + PascalCase | `GwTenant.cs`, `GwTenantRoute.cs` | +| 配置类 | `*Config` 后缀 | `JwtConfig.cs`, `RedisConfig.cs` | +| 提供者 | `*Provider` 后缀 | `DatabaseRouteConfigProvider.cs` | +| 中间件 | `*Middleware` 后缀 | `JwtTransformMiddleware.cs` | +| 控制器 | `*Controller` 后缀 | `GatewayConfigController.cs` | +| 服务 | 功能描述 + 类型 | `RouteCache.cs`, `PgSqlConfigChangeListener.cs` | +| 策略 | `*Policy` 后缀 | `DistributedWeightedRoundRobinPolicy.cs` | + +### 4.2 命名空间 + +``` +YarpGateway # 根命名空间 +├── Config # 配置相关 +├── Controllers # API 控制器 +├── Data # 数据访问 +├── DynamicProxy # 动态代理 +├── LoadBalancing # 负载均衡 +├── Metrics # 监控指标 +├── Middleware # 中间件 +├── Models # 数据模型 +└── Services # 业务服务 +``` + +### 4.3 接口命名 + +| 类型 | 命名规则 | 示例 | +|------|----------|------| +| 服务接口 | `I` 前缀 | `IRouteCache`, `IRedisConnectionManager` | +| DTO 类 | `*Dto` 后缀 | `CreateTenantDto`, `CreateRouteDto` | +| 请求类 | `*Request` 后缀 | `AssignServiceRequest` | + +--- + +## 5. 模块组织 + +### 5.1 分层架构 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Presentation Layer │ +│ ┌─────────────────┐ ┌─────────────────────────────────┐ │ +│ │ Middleware │ │ Controllers │ │ +│ │ - JWT 解析 │ │ - GatewayConfigController │ │ +│ │ - 租户路由 │ │ - PendingServicesController │ │ +│ └─────────────────┘ └─────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Business Logic Layer │ +│ ┌─────────────────┐ ┌─────────────────────────────────┐ │ +│ │ Services │ │ DynamicProxy │ │ +│ │ - RouteCache │ │ - DynamicProxyConfigProvider │ │ +│ │ - RedisManager │ │ │ │ +│ │ - ConfigListen │ └─────────────────────────────────┘ │ +│ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Data Access Layer │ +│ ┌─────────────────┐ ┌─────────────────────────────────┐ │ +│ │ Models │ │ Data │ │ +│ │ - GwTenant │ │ - GatewayDbContext │ │ +│ │ - GwRoute │ │ - GatewayDbContextFactory │ │ +│ │ - GwInstance │ │ │ │ +│ └─────────────────┘ └─────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────────┐ +│ Infrastructure Layer │ +│ ┌─────────────────┐ ┌─────────────────────────────────┐ │ +│ │ Config │ │ LoadBalancing │ │ +│ │ - JwtConfig │ │ - WeightedRoundRobinPolicy │ │ +│ │ - RedisConfig │ │ │ │ +│ │ - Providers │ └─────────────────────────────────┘ │ +│ └─────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### 5.2 模块依赖关系 + +``` +Program.cs + │ + ├── Config/ + │ ├── JwtConfig ◄── appsettings.json + │ ├── RedisConfig ◄── appsettings.json + │ ├── DatabaseRouteConfigProvider ◄── Data/GatewayDbContext + │ └── DatabaseClusterConfigProvider ◄── Data/GatewayDbContext + │ + ├── DynamicProxy/ + │ └── DynamicProxyConfigProvider ◄── Config/* + │ + ├── Services/ + │ ├── RouteCache ◄── Data/GatewayDbContext, Models/* + │ ├── RedisConnectionManager ◄── Config/RedisConfig + │ ├── PgSqlConfigChangeListener ◄── DynamicProxy, Services/RouteCache + │ └── KubernetesPendingSyncService ◄── Data/GatewayDbContext + │ + ├── Middleware/ + │ ├── JwtTransformMiddleware ◄── Config/JwtConfig + │ └── TenantRoutingMiddleware ◄── Services/RouteCache + │ + └── Controllers/ + ├── GatewayConfigController ◄── Config/*, Services/RouteCache + └── PendingServicesController ◄── Data/GatewayDbContext +``` + +--- + +## 6. 项目依赖 + +### 6.1 NuGet 包引用 + +```xml + + + + + + + + + + + + + + + + + + + + +``` + +### 6.2 目标框架 + +```xml +net10.0 +``` + +--- + +## 7. 文件统计 + +| 目录/文件 | 文件数 | 总行数 | 主要用途 | +|-----------|--------|--------|----------| +| `Config/` | 5 | ~210 | 配置模型和提供者 | +| `Controllers/` | 2 | ~700 | REST API 端点 | +| `Data/` | 2 | ~165 | 数据库上下文 | +| `DynamicProxy/` | 1 | ~79 | YARP 配置集成 | +| `LoadBalancing/` | 1 | ~244 | 负载均衡策略 | +| `Migrations/` | 6 | ~500+ | 数据库迁移 | +| `Metrics/` | 1 | ~31 | 监控指标 | +| `Middleware/` | 2 | ~148 | 请求处理中间件 | +| `Models/` | 4 | ~83 | 数据实体 | +| `Services/` | 4 | ~665 | 业务服务 | +| `Program.cs` | 1 | 135 | 应用入口 | +| **总计** | **29** | **~2900+** | - | + +--- + +## 8. 扩展建议 + +### 8.1 建议新增目录 + +| 目录 | 用途 | +|------|------| +| `Extensions/` | 扩展方法 | +| `Constants/` | 常量定义 | +| `Exceptions/` | 自定义异常 | +| `Validators/` | 输入验证器 | +| `Dtos/` | 数据传输对象(从 Controllers 提取) | + +### 8.2 代码组织建议 + +1. 将 Controller 中的 DTO 类提取到独立的 `Dtos/` 目录 +2. 添加 `Extensions/` 存放 IServiceCollection 扩展方法 +3. 考虑将配置验证逻辑提取到 `Validators/` \ No newline at end of file diff --git a/.planning/codebase/TESTING.md b/.planning/codebase/TESTING.md new file mode 100644 index 0000000..6056908 --- /dev/null +++ b/.planning/codebase/TESTING.md @@ -0,0 +1,833 @@ +# YARP Gateway 测试文档 + +## 概述 + +本文档记录了 YARP Gateway 项目的测试策略、测试模式和最佳实践。 + +--- + +## 1. 测试框架 + +### 1.1 当前测试状态 + +**项目当前没有专门的测试目录或测试项目。** + +检查项目结构: +``` +fengling-gateway/ +├── src/ # 源代码 +│ └── YarpGateway.csproj # 主项目 +├── .planning/ +└── (无 tests/ 或 test/ 目录) +``` + +检查 `.csproj` 文件确认无测试框架依赖: +```xml + + + + + + + + +``` + +**结论**:项目目前处于开发阶段,尚未建立测试基础设施。 + +### 1.2 推荐测试框架 + +基于项目技术栈,推荐以下测试框架: + +| 框架 | 用途 | NuGet 包 | +|------|------|----------| +| xUnit | 单元测试框架 | `xunit` | +| Moq | Mock 框架 | `Moq` | +| FluentAssertions | 断言库 | `FluentAssertions` | +| Microsoft.NET.Test.Sdk | 测试 SDK | `Microsoft.NET.Test.Sdk` | +| Testcontainers | 集成测试容器 | `Testcontainers.PostgreSql`, `Testcontainers.Redis` | + +--- + +## 2. 推荐测试结构 + +### 2.1 测试项目组织 + +建议创建独立的测试项目: + +``` +tests/ +├── YarpGateway.UnitTests/ # 单元测试 +│ ├── Services/ +│ │ ├── RouteCacheTests.cs +│ │ └── RedisConnectionManagerTests.cs +│ ├── Middleware/ +│ │ ├── JwtTransformMiddlewareTests.cs +│ │ └── TenantRoutingMiddlewareTests.cs +│ └── Controllers/ +│ └── GatewayConfigControllerTests.cs +│ +├── YarpGateway.IntegrationTests/ # 集成测试 +│ ├── GatewayEndpointsTests.cs +│ └── DatabaseTests.cs +│ +└── YarpGateway.LoadTests/ # 负载测试(可选) + └── RoutePerformanceTests.cs +``` + +### 2.2 测试命名约定 + +```csharp +// 命名格式:[被测类]Tests +public class RouteCacheTests { } + +// 方法命名格式:[方法名]_[场景]_[期望结果] +[Fact] +public async Task InitializeAsync_WithValidData_LoadsRoutesFromDatabase() { } + +[Fact] +public async Task GetRoute_WithNonexistentTenant_ReturnsNull() { } + +[Fact] +public async Task ReloadAsync_WhenCalled_RefreshesCache() { } +``` + +--- + +## 3. 单元测试模式 + +### 3.1 服务层测试示例 + +```csharp +// RouteCacheTests.cs +using Xunit; +using Moq; +using FluentAssertions; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; + +public class RouteCacheTests +{ + private readonly Mock> _mockDbContextFactory; + private readonly Mock> _mockLogger; + private readonly RouteCache _sut; // System Under Test + + public RouteCacheTests() + { + _mockDbContextFactory = new Mock>(); + _mockLogger = new Mock>(); + _sut = new RouteCache(_mockDbContextFactory.Object, _mockLogger.Object); + } + + [Fact] + public async Task InitializeAsync_ShouldLoadRoutesFromDatabase() + { + // Arrange + var routes = new List + { + new() { Id = 1, ServiceName = "user-service", ClusterId = "user-cluster", IsGlobal = true } + }; + + var mockDbSet = CreateMockDbSet(routes); + var mockContext = new Mock(); + mockContext.Setup(c => c.TenantRoutes).Returns(mockDbSet.Object); + + _mockDbContextFactory + .Setup(f => f.CreateDbContext()) + .Returns(mockContext.Object); + + // Act + await _sut.InitializeAsync(); + + // Assert + var result = _sut.GetRoute("tenant1", "user-service"); + result.Should().NotBeNull(); + result!.ClusterId.Should().Be("user-cluster"); + } + + [Fact] + public async Task GetRoute_WhenTenantRouteExists_ReturnsTenantRoute() + { + // Arrange - 设置租户专用路由 + // ... + + // Act + var result = _sut.GetRoute("tenant1", "service1"); + + // Assert + result.Should().NotBeNull(); + result!.IsGlobal.Should().BeFalse(); + } + + [Fact] + public async Task GetRoute_WhenNoTenantRouteButGlobalExists_ReturnsGlobalRoute() + { + // Arrange + // ... + + // Act + var result = _sut.GetRoute("tenant-without-route", "global-service"); + + // Assert + result.Should().NotBeNull(); + result!.IsGlobal.Should().BeTrue(); + } + + // 辅助方法:创建模拟 DbSet + private Mock> CreateMockDbSet(List data) where T : class + { + var queryable = data.AsQueryable(); + var mockSet = new Mock>(); + mockSet.As>().Setup(m => m.Provider).Returns(queryable.Provider); + mockSet.As>().Setup(m => m.Expression).Returns(queryable.Expression); + mockSet.As>().Setup(m => m.ElementType).Returns(queryable.ElementType); + mockSet.As>().Setup(m => m.GetEnumerator()).Returns(queryable.GetEnumerator()); + return mockSet; + } +} +``` + +### 3.2 中间件测试示例 + +```csharp +// TenantRoutingMiddlewareTests.cs +using Xunit; +using Moq; +using FluentAssertions; +using Microsoft.AspNetCore.Http; + +public class TenantRoutingMiddlewareTests +{ + private readonly Mock _mockNext; + private readonly Mock _mockRouteCache; + private readonly Mock> _mockLogger; + private readonly TenantRoutingMiddleware _sut; + + public TenantRoutingMiddlewareTests() + { + _mockNext = new Mock(); + _mockRouteCache = new Mock(); + _mockLogger = new Mock>(); + _sut = new TenantRoutingMiddleware(_mockNext.Object, _mockRouteCache.Object, _mockLogger.Object); + } + + [Fact] + public async Task InvokeAsync_WithoutTenantHeader_CallsNextWithoutProcessing() + { + // Arrange + var context = new DefaultHttpContext(); + context.Request.Path = "/api/user-service/users"; + + // Act + await _sut.InvokeAsync(context); + + // Assert + _mockNext.Verify(n => n(context), Times.Once); + _mockRouteCache.Verify(r => r.GetRoute(It.IsAny(), It.IsAny()), Times.Never); + } + + [Fact] + public async Task InvokeAsync_WithValidTenantAndRoute_SetsDynamicClusterId() + { + // Arrange + var context = new DefaultHttpContext(); + context.Request.Path = "/api/order-service/orders"; + context.Request.Headers["X-Tenant-Id"] = "tenant-123"; + + var routeInfo = new RouteInfo + { + ClusterId = "order-cluster", + IsGlobal = false + }; + + _mockRouteCache + .Setup(r => r.GetRoute("tenant-123", "order-service")) + .Returns(routeInfo); + + // Act + await _sut.InvokeAsync(context); + + // Assert + context.Items["DynamicClusterId"].Should().Be("order-cluster"); + _mockNext.Verify(n => n(context), Times.Once); + } + + [Fact] + public async Task InvokeAsync_WithNoMatchingRoute_CallsNextWithoutClusterId() + { + // Arrange + var context = new DefaultHttpContext(); + context.Request.Path = "/api/unknown-service/data"; + context.Request.Headers["X-Tenant-Id"] = "tenant-123"; + + _mockRouteCache + .Setup(r => r.GetRoute("tenant-123", "unknown-service")) + .Returns((RouteInfo?)null); + + // Act + await _sut.InvokeAsync(context); + + // Assert + context.Items.ContainsKey("DynamicClusterId").Should().BeFalse(); + _mockNext.Verify(n => n(context), Times.Once); + } +} +``` + +### 3.3 控制器测试示例 + +```csharp +// GatewayConfigControllerTests.cs +using Xunit; +using Moq; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; + +public class GatewayConfigControllerTests +{ + private readonly Mock> _mockDbFactory; + private readonly Mock _mockRouteProvider; + private readonly Mock _mockClusterProvider; + private readonly Mock _mockRouteCache; + private readonly GatewayConfigController _sut; + + public GatewayConfigControllerTests() + { + _mockDbFactory = new Mock>(); + _mockRouteProvider = new Mock(); + _mockClusterProvider = new Mock(); + _mockRouteCache = new Mock(); + + _sut = new GatewayConfigController( + _mockDbFactory.Object, + _mockRouteProvider.Object, + _mockClusterProvider.Object, + _mockRouteCache.Object + ); + } + + [Fact] + public async Task GetTenants_ShouldReturnPaginatedList() + { + // Arrange + var tenants = new List + { + new() { Id = 1, TenantCode = "tenant1", TenantName = "Tenant 1" }, + new() { Id = 2, TenantCode = "tenant2", TenantName = "Tenant 2" } + }; + + // 设置模拟 DbContext... + + // Act + var result = await _sut.GetTenants(page: 1, pageSize: 10); + + // Assert + var okResult = result.Should().BeOfType().Subject; + var response = okResult.Value.Should().BeAnonymousType(); + response.Property("total").Should().Be(2); + } + + [Fact] + public async Task CreateTenant_WithValidData_ReturnsCreatedTenant() + { + // Arrange + var dto = new GatewayConfigController.CreateTenantDto + { + TenantCode = "new-tenant", + TenantName = "New Tenant" + }; + + // Act + var result = await _sut.CreateTenant(dto); + + // Assert + var okResult = result.Should().BeOfType().Subject; + okResult.Value.Should().BeAssignableTo(); + } + + [Fact] + public async Task DeleteTenant_WithNonexistentId_ReturnsNotFound() + { + // Arrange + // 设置模拟返回 null + + // Act + var result = await _sut.DeleteTenant(999); + + // Assert + result.Should().BeOfType(); + } +} +``` + +--- + +## 4. Mock 模式 + +### 4.1 接口 Mock + +```csharp +// 使用 Moq 模拟接口 +public class RouteCacheTests +{ + private readonly Mock _mockRouteCache; + + public RouteCacheTests() + { + _mockRouteCache = new Mock(); + } + + [Fact] + public async Task TestMethod() + { + // 设置返回值 + _mockRouteCache + .Setup(r => r.GetRoute("tenant1", "service1")) + .Returns(new RouteInfo { ClusterId = "cluster1" }); + + // 设置异步方法 + _mockRouteCache + .Setup(r => r.InitializeAsync()) + .Returns(Task.CompletedTask); + + // 验证调用 + _mockRouteCache.Verify(r => r.GetRoute(It.IsAny(), It.IsAny()), Times.Once); + } +} +``` + +### 4.2 DbContext Mock + +```csharp +// 使用 In-Memory 数据库进行测试 +public class TestDatabaseFixture +{ + public GatewayDbContext CreateContext() + { + var options = new DbContextOptionsBuilder() + .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) + .Options; + + var context = new GatewayDbContext(options); + + // 种子数据 + context.Tenants.Add(new GwTenant { Id = 1, TenantCode = "test-tenant" }); + context.TenantRoutes.Add(new GwTenantRoute + { + Id = 1, + ServiceName = "test-service", + ClusterId = "test-cluster" + }); + context.SaveChanges(); + + return context; + } +} + +public class GatewayDbContextTests : IClassFixture +{ + private readonly TestDatabaseFixture _fixture; + + public GatewayDbContextTests(TestDatabaseFixture fixture) + { + _fixture = fixture; + } + + [Fact] + public async Task SaveChangesAsync_ShouldNotifyConfigChange() + { + // Arrange + await using var context = _fixture.CreateContext(); + + // Act + var route = new GwTenantRoute { ServiceName = "new-service", ClusterId = "new-cluster" }; + context.TenantRoutes.Add(route); + await context.SaveChangesAsync(); + + // Assert + // 验证通知行为(如果需要) + } +} +``` + +### 4.3 Redis Mock + +```csharp +// 使用 Moq 模拟 Redis +public class RedisConnectionManagerTests +{ + private readonly Mock _mockRedis; + private readonly Mock _mockDatabase; + + public RedisConnectionManagerTests() + { + _mockRedis = new Mock(); + _mockDatabase = new Mock(); + _mockRedis.Setup(r => r.GetDatabase(It.IsAny(), It.IsAny())) + .Returns(_mockDatabase.Object); + } + + [Fact] + public async Task AcquireLockAsync_WhenLockAvailable_ReturnsDisposable() + { + // Arrange + _mockDatabase + .Setup(d => d.StringSetAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(true); + + // Act & Assert + // 测试逻辑... + } +} +``` + +--- + +## 5. 集成测试模式 + +### 5.1 WebApplicationFactory 模式 + +```csharp +// 使用 WebApplicationFactory 进行 API 集成测试 +using Microsoft.AspNetCore.Mvc.Testing; + +public class GatewayIntegrationTests : IClassFixture> +{ + private readonly WebApplicationFactory _factory; + private readonly HttpClient _client; + + public GatewayIntegrationTests(WebApplicationFactory factory) + { + _factory = factory.WithWebHostBuilder(builder => + { + builder.ConfigureServices(services => + { + // 替换真实服务为测试替身 + services.RemoveAll>(); + services.AddDbContextFactory(options => + options.UseInMemoryDatabase("TestDb")); + }); + }); + _client = _factory.CreateClient(); + } + + [Fact] + public async Task GetHealth_ReturnsHealthy() + { + // Act + var response = await _client.GetAsync("/health"); + + // Assert + response.Should().BeSuccessful(); + var content = await response.Content.ReadAsStringAsync(); + content.Should().Contain("healthy"); + } + + [Fact] + public async Task GetTenants_ReturnsPaginatedList() + { + // Act + var response = await _client.GetAsync("/api/gateway/tenants?page=1&pageSize=10"); + + // Assert + response.Should().BeSuccessful(); + // 进一步验证响应内容... + } +} +``` + +### 5.2 Testcontainers 模式 + +```csharp +// 使用 Testcontainers 进行真实数据库集成测试 +using Testcontainers.PostgreSql; +using Testcontainers.Redis; + +public class DatabaseIntegrationTests : IAsyncLifetime +{ + private readonly PostgreSqlContainer _postgresContainer; + private readonly RedisContainer _redisContainer; + + public DatabaseIntegrationTests() + { + _postgresContainer = new PostgreSqlBuilder() + .WithImage("postgres:15-alpine") + .WithDatabase("test_gateway") + .WithUsername("test") + .WithPassword("test") + .Build(); + + _redisContainer = new RedisBuilder() + .WithImage("redis:7-alpine") + .Build(); + } + + public async Task InitializeAsync() + { + await _postgresContainer.StartAsync(); + await _redisContainer.StartAsync(); + } + + public async Task DisposeAsync() + { + await _postgresContainer.DisposeAsync(); + await _redisContainer.DisposeAsync(); + } + + [Fact] + public async Task FullWorkflow_CreateTenantAndRoute_RouteShouldWork() + { + // Arrange + var connectionString = _postgresContainer.GetConnectionString(); + + // 使用真实连接进行端到端测试... + } +} +``` + +--- + +## 6. 测试覆盖率 + +### 6.1 当前状态 + +项目当前无测试覆盖率数据。 + +### 6.2 推荐覆盖率目标 + +| 层级 | 目标覆盖率 | 说明 | +|------|-----------|------| +| Services | 80%+ | 核心业务逻辑,必须高覆盖 | +| Middleware | 75%+ | 关键请求处理逻辑 | +| Controllers | 70%+ | API 端点行为验证 | +| Config | 60%+ | 配置加载和验证 | +| Models | 30%+ | 简单 POCO 类,低优先级 | + +### 6.3 配置覆盖率收集 + +```xml + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + +``` + +```bash +# 运行测试并收集覆盖率 +dotnet test --collect:"XPlat Code Coverage" + +# 生成覆盖率报告 +dotnet tool install -g dotnet-reportgenerator-globaltool +reportgenerator -reports:"**/coverage.cobertura.xml" -targetdir:"coverage-report" +``` + +--- + +## 7. 如何运行测试 + +### 7.1 运行所有测试 + +```bash +# 运行所有测试 +dotnet test + +# 运行特定项目 +dotnet test tests/YarpGateway.UnitTests + +# 运行特定测试类 +dotnet test --filter "FullyQualifiedName~RouteCacheTests" + +# 运行特定测试方法 +dotnet test --filter "FullyQualifiedName~RouteCacheTests.InitializeAsync_ShouldLoadRoutesFromDatabase" +``` + +### 7.2 运行测试类别 + +```csharp +// 定义测试类别 +[Trait("Category", "Unit")] +public class RouteCacheTests { } + +[Trait("Category", "Integration")] +public class GatewayIntegrationTests { } +``` + +```bash +# 只运行单元测试 +dotnet test --filter "Category=Unit" + +# 排除集成测试 +dotnet test --filter "Category!=Integration" +``` + +### 7.3 CI/CD 配置示例 + +```yaml +# .github/workflows/test.yml +name: Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:15 + env: + POSTGRES_DB: test_gateway + POSTGRES_USER: test + POSTGRES_PASSWORD: test + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + redis: + image: redis:7 + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.0.x' + + - name: Restore dependencies + run: dotnet restore + + - name: Build + run: dotnet build --configuration Release --no-restore + + - name: Test + run: dotnet test --configuration Release --no-build --verbosity normal --collect:"XPlat Code Coverage" + env: + ConnectionStrings__DefaultConnection: "Host=localhost;Database=test_gateway;Username=test;Password=test" + Redis__ConnectionString: "localhost:6379" + + - name: Upload coverage + uses: codecov/codecov-action@v3 + with: + files: ./tests/**/coverage.cobertura.xml +``` + +--- + +## 8. 测试最佳实践 + +### 8.1 AAA 模式 + +```csharp +[Fact] +public async Task Method_Scenario_ExpectedResult() +{ + // Arrange - 准备测试数据和环境 + var input = "test-data"; + + // Act - 执行被测试的方法 + var result = await _sut.MethodAsync(input); + + // Assert - 验证结果 + result.Should().Be(expected); +} +``` + +### 8.2 单一职责 + +```csharp +// ✅ 好:每个测试只验证一个行为 +[Fact] +public async Task CreateTenant_WithValidData_ReturnsCreatedTenant() { } + +[Fact] +public async Task CreateTenant_WithDuplicateCode_ReturnsBadRequest() { } + +// ❌ 差:一个测试验证多个行为 +[Fact] +public async Task CreateTenant_TestsAllScenarios() { } +``` + +### 8.3 测试隔离 + +```csharp +public class RouteCacheTests +{ + // 每个测试使用独立实例 + private readonly RouteCache _sut; + + public RouteCacheTests() + { + // 在构造函数中初始化,确保每个测试独立 + _sut = new RouteCache(...); + } +} +``` + +### 8.4 避免实现细节测试 + +```csharp +// ✅ 好:测试行为而非实现 +[Fact] +public async Task GetRoute_ReturnsCorrectRoute() { } + +// ❌ 差:测试内部实现细节 +[Fact] +public void InternalDictionary_ContainsCorrectKey() { } +``` + +--- + +## 9. 总结 + +### 当前状态 +- ❌ 无测试项目 +- ❌ 无测试框架依赖 +- ❌ 无测试覆盖率 +- ❌ 无 CI/CD 测试配置 + +### 建议行动计划 + +1. **创建测试项目** + ```bash + dotnet new xunit -n YarpGateway.UnitTests -o tests/YarpGateway.UnitTests + dotnet new xunit -n YarpGateway.IntegrationTests -o tests/YarpGateway.IntegrationTests + ``` + +2. **添加测试依赖** + ```bash + dotnet add package Moq + dotnet add package FluentAssertions + dotnet add package coverlet.collector + ``` + +3. **优先测试核心服务** + - `RouteCache` - 路由缓存核心逻辑 + - `RedisConnectionManager` - Redis 连接和分布式锁 + - `TenantRoutingMiddleware` - 租户路由中间件 + +4. **建立 CI/CD 测试流程** + - 每次提交运行单元测试 + - 每次合并运行集成测试 + - 生成覆盖率报告 + +通过建立完善的测试体系,可以显著提高代码质量和项目可维护性。 \ No newline at end of file