# 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. **健壮性**:完善的错误处理和并发控制 遵循这些约定可以确保代码质量和团队协作效率。