fengling-gateway/.planning/codebase/CONVENTIONS.md
2026-02-28 15:44:16 +08:00

690 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<TenantRoutingMiddleware> _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<JwtConfig>(builder.Configuration.GetSection("Jwt"));
builder.Services.Configure<RedisConfig>(builder.Configuration.GetSection("Redis"));
// 直接注册配置实例(当需要直接使用配置对象时)
builder.Services.AddSingleton(sp => sp.GetRequiredService<IOptions<RedisConfig>>().Value);
// DbContext 使用工厂模式
builder.Services.AddDbContextFactory<GatewayDbContext>(options =>
options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))
);
// 单例服务(无状态或线程安全)
builder.Services.AddSingleton<DatabaseRouteConfigProvider>();
builder.Services.AddSingleton<DatabaseClusterConfigProvider>();
builder.Services.AddSingleton<IRouteCache, RouteCache>();
// 接口与实现分离注册
builder.Services.AddSingleton<IRedisConnectionManager, RedisConnectionManager>();
// 后台服务
builder.Services.AddHostedService<PgSqlConfigChangeListener>();
builder.Services.AddHostedService<KubernetesPendingSyncService>();
```
### 2.2 依赖注入构造函数模式
```csharp
public class RouteCache : IRouteCache
{
private readonly IDbContextFactory<GatewayDbContext> _dbContextFactory;
private readonly ILogger<RouteCache> _logger;
public RouteCache(
IDbContextFactory<GatewayDbContext> dbContextFactory,
ILogger<RouteCache> logger)
{
_dbContextFactory = dbContextFactory;
_logger = logger;
}
}
```
**模式要点**
1. 所有依赖通过构造函数注入
2. 使用 `readonly` 修饰私有字段
3. 依赖项按类别排序(框架 → 基础设施 → 业务服务)
**原因**:构造函数注入确保依赖不可变,便于测试和依赖管理。
### 2.3 IDbContextFactory 模式
```csharp
// 在 Singleton 服务中使用 DbContextFactory
public class RouteCache : IRouteCache
{
private readonly IDbContextFactory<GatewayDbContext> _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<GatewayDbContext>>();
// ...
}
}
```
**原因**`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<JwtConfig>(builder.Configuration.GetSection("Jwt"));
// 通过 IOptions<T> 注入
public class JwtTransformMiddleware
{
private readonly JwtConfig _jwtConfig;
public JwtTransformMiddleware(
RequestDelegate next,
IOptions<JwtConfig> jwtConfig, // 使用 IOptions<T>
ILogger<JwtTransformMiddleware> 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<int> 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<IActionResult> 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<IActionResult> 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<IDisposable> 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<TenantRoutingMiddleware> _logger;
// 构造函数注入依赖
public TenantRoutingMiddleware(
RequestDelegate next,
IRouteCache routeCache,
ILogger<TenantRoutingMiddleware> 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<JwtTransformMiddleware>(); // JWT 解析
app.UseMiddleware<TenantRoutingMiddleware>(); // 租户路由
app.MapControllers();
app.MapReverseProxy();
```
**顺序原因**
1. CORS 需最先处理跨域请求
2. JWT 中间件解析用户信息供后续使用
3. 租户路由根据用户信息选择目标服务
---
## 8. 控制器约定
### 8.1 控制器结构
```csharp
[ApiController]
[Route("api/gateway")]
public class GatewayConfigController : ControllerBase
{
private readonly IDbContextFactory<GatewayDbContext> _dbContextFactory;
private readonly IRouteCache _routeCache;
public GatewayConfigController(
IDbContextFactory<GatewayDbContext> dbContextFactory,
IRouteCache routeCache)
{
_dbContextFactory = dbContextFactory;
_routeCache = routeCache;
}
#region Tenants
// 租户相关端点
#endregion
#region Routes
// 路由相关端点
#endregion
}
```
### 8.2 端点命名
```csharp
// GET 集合
[HttpGet("tenants")]
public async Task<IActionResult> GetTenants(...) { }
// GET 单个
[HttpGet("tenants/{id}")]
public async Task<IActionResult> GetTenant(long id) { }
// POST 创建
[HttpPost("tenants")]
public async Task<IActionResult> CreateTenant([FromBody] CreateTenantDto dto) { }
// PUT 更新
[HttpPut("tenants/{id}")]
public async Task<IActionResult> UpdateTenant(long id, [FromBody] UpdateTenantDto dto) { }
// DELETE 删除
[HttpDelete("tenants/{id}")]
public async Task<IActionResult> DeleteTenant(long id) { }
```
---
## 9. 总结
本项目的编码约定遵循以下核心原则:
1. **一致性**:统一的命名和代码组织方式
2. **可测试性**:依赖注入和接口抽象便于测试
3. **可维护性**:清晰的结构和文档注释
4. **可观测性**:结构化日志和指标收集
5. **健壮性**:完善的错误处理和并发控制
遵循这些约定可以确保代码质量和团队协作效率。