17 KiB
17 KiB
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;
}
}
模式要点:
- 所有依赖通过构造函数注入
- 使用
readonly修饰私有字段 - 依赖项按类别排序(框架 → 基础设施 → 业务服务)
原因:构造函数注入确保依赖不可变,便于测试和依赖管理。
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 = "..." });
}
模式要点:
- 使用早期返回(Guard Clauses)减少嵌套
- 返回结构化的错误信息
- 使用 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);
模式要点:
- 使用占位符
{PropertyName}而非字符串插值 - 日志消息使用常量,便于聚合分析
- 包含足够的上下文信息
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();
顺序原因:
- CORS 需最先处理跨域请求
- JWT 中间件解析用户信息供后续使用
- 租户路由根据用户信息选择目标服务
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. 总结
本项目的编码约定遵循以下核心原则:
- 一致性:统一的命名和代码组织方式
- 可测试性:依赖注入和接口抽象便于测试
- 可维护性:清晰的结构和文档注释
- 可观测性:结构化日志和指标收集
- 健壮性:完善的错误处理和并发控制
遵循这些约定可以确保代码质量和团队协作效率。