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

17 KiB
Raw Blame History

YARP Gateway 编码约定文档

概述

本文档记录了 YARP Gateway 项目的编码约定和最佳实践,旨在帮助开发人员理解和遵循项目规范。


1. 代码风格

1.1 命名约定

类和接口命名

// 接口:使用 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;
    // ...
}

私有字段命名

// 使用下划线前缀 + camelCase
public class TenantRoutingMiddleware
{
    private readonly RequestDelegate _next;
    private readonly IRouteCache _routeCache;
    private readonly ILogger<TenantRoutingMiddleware> _logger;
}

原因:下划线前缀清晰区分私有字段和局部变量,避免 this. 的频繁使用。

方法命名

// 异步方法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 服务注册

// 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 依赖注入构造函数模式

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 模式

// 在 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 配置类定义

// 简单 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 配置绑定和注入

// 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 动态配置更新

// 配置变更通知通道
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 中间件错误处理

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 控制器错误处理

[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 后台服务错误处理

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 结构化日志

// 使用 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 配置

// 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 使用

// 正确:异步方法使用 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 使用

// 控制器方法
[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 并发控制

// 使用 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 分布式锁模式

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 标准中间件结构

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 中间件注册顺序

// 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 控制器结构

[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 端点命名

// 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. 健壮性:完善的错误处理和并发控制

遵循这些约定可以确保代码质量和团队协作效率。