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