325 lines
12 KiB
Markdown
325 lines
12 KiB
Markdown
# Phase 3: 调整网关部分的需求 - 技术研究
|
|
|
|
**日期:** 2026-03-03
|
|
**状态:** 研究完成
|
|
|
|
---
|
|
|
|
## 1. EF Core Owned Entity 配置模式
|
|
|
|
### GwDestination 内嵌实体
|
|
|
|
```csharp
|
|
// 在 GwCluster 中配置 Owned Entity
|
|
modelBuilder.Entity<GwCluster>(entity =>
|
|
{
|
|
entity.HasKey(e => e.Id);
|
|
entity.Property(e => e.ClusterId).HasMaxLength(100).IsRequired();
|
|
entity.HasIndex(e => e.ClusterId).IsUnique();
|
|
|
|
// 配置内嵌的 Destinations 列表
|
|
entity.OwnsMany(e => e.Destinations, dest =>
|
|
{
|
|
dest.WithOwner().HasForeignKey("GwClusterId");
|
|
dest.Property(d => d.DestinationId).HasMaxLength(100).IsRequired();
|
|
dest.Property(d => d.Address).HasMaxLength(200).IsRequired();
|
|
dest.Property(d => d.Health).HasMaxLength(200);
|
|
dest.Property(d => d.Weight).HasDefaultValue(1);
|
|
dest.Property(d => d.HealthStatus).HasDefaultValue(1);
|
|
dest.Property(d => d.Status).HasDefaultValue(1);
|
|
|
|
// 复合唯一索引
|
|
dest.HasIndex(d => new { d.DestinationId }).IsUnique();
|
|
});
|
|
});
|
|
```
|
|
|
|
### GwHealthCheckConfig 内嵌值对象
|
|
|
|
```csharp
|
|
entity.OwnsOne(e => e.HealthCheck, hc =>
|
|
{
|
|
hc.Property(h => h.Enabled).HasDefaultValue(false);
|
|
hc.Property(h => h.Path).HasMaxLength(100).HasDefaultValue("/health");
|
|
hc.Property(h => h.IntervalSeconds).HasDefaultValue(30);
|
|
hc.Property(h => h.TimeoutSeconds).HasDefaultValue(10);
|
|
});
|
|
```
|
|
|
|
### GwSessionAffinityConfig 内嵌值对象
|
|
|
|
```csharp
|
|
entity.OwnsOne(e => e.SessionAffinity, sa =>
|
|
{
|
|
sa.Property(s => s.Enabled).HasDefaultValue(false);
|
|
sa.Property(s => s.Policy).HasMaxLength(50).HasDefaultValue("Header");
|
|
sa.Property(s => s.AffinityKeyName).HasMaxLength(100).HasDefaultValue("X-Session-Key");
|
|
});
|
|
```
|
|
|
|
---
|
|
|
|
## 2. 实体迁移策略
|
|
|
|
### 删除现有实体
|
|
|
|
```csharp
|
|
// 1. 从 PlatformDbContext 中移除 DbSet
|
|
public DbSet<GwTenant> GwTenants => Set<GwTenant>(); // 删除
|
|
public DbSet<GwServiceInstance> GwServiceInstances => Set<GwServiceInstance>(); // 删除
|
|
|
|
// 2. 移除 OnModelCreating 中的配置
|
|
// modelBuilder.Entity<GwTenant>(...) - 删除
|
|
// modelBuilder.Entity<GwServiceInstance>(...) - 删除
|
|
```
|
|
|
|
### 创建 EF Core 迁移
|
|
|
|
```bash
|
|
# 创建迁移
|
|
dotnet ef migrations add RestructureGatewayEntities --project Fengling.Platform.Infrastructure
|
|
|
|
# 迁移将执行:
|
|
# - DROP TABLE GwTenants
|
|
# - DROP TABLE GwServiceInstances
|
|
# - CREATE TABLE GwClusters (包含 Destinations 作为 JSON 或关联表)
|
|
# - ALTER TABLE GwTenantRoutes (添加新字段)
|
|
```
|
|
|
|
---
|
|
|
|
## 3. GwCluster → YARP ClusterConfig 映射
|
|
|
|
```csharp
|
|
public static ClusterConfig ToClusterConfig(this GwCluster cluster)
|
|
{
|
|
return new ClusterConfig
|
|
{
|
|
ClusterId = cluster.ClusterId,
|
|
LoadBalancingPolicy = cluster.LoadBalancingPolicy ?? "PowerOfTwoChoices",
|
|
Destinations = cluster.Destinations
|
|
.Where(d => d.Status == 1)
|
|
.ToDictionary(
|
|
d => d.DestinationId,
|
|
d => new DestinationConfig
|
|
{
|
|
Address = d.Address,
|
|
Health = d.Health,
|
|
Metadata = new Dictionary<string, string>
|
|
{
|
|
["Weight"] = d.Weight.ToString()
|
|
}
|
|
}
|
|
),
|
|
HealthCheck = cluster.HealthCheck?.Enabled == true
|
|
? new HealthCheckConfig
|
|
{
|
|
Active = new ActiveHealthCheckConfig
|
|
{
|
|
Enabled = true,
|
|
Path = cluster.HealthCheck.Path ?? "/health",
|
|
Interval = TimeSpan.FromSeconds(cluster.HealthCheck.IntervalSeconds),
|
|
Timeout = TimeSpan.FromSeconds(cluster.HealthCheck.TimeoutSeconds)
|
|
}
|
|
}
|
|
: null,
|
|
SessionAffinity = cluster.SessionAffinity?.Enabled == true
|
|
? new SessionAffinityConfig
|
|
{
|
|
Enabled = true,
|
|
Policy = cluster.SessionAffinity.Policy,
|
|
AffinityKeyName = cluster.SessionAffinity.AffinityKeyName
|
|
}
|
|
: null
|
|
};
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. GwTenantRoute → YARP RouteConfig 映射
|
|
|
|
```csharp
|
|
public static RouteConfig ToRouteConfig(this GwTenantRoute route)
|
|
{
|
|
return new RouteConfig
|
|
{
|
|
RouteId = route.Id,
|
|
Match = new RouteMatch
|
|
{
|
|
Path = route.PathPattern,
|
|
Methods = route.Methods?.Split(',').ToList(),
|
|
Hosts = route.Hosts?.Split(',').ToList(),
|
|
Headers = ParseHeaderMatch(route.Headers)
|
|
},
|
|
ClusterId = route.ClusterId,
|
|
Order = route.Priority,
|
|
Metadata = new Dictionary<string, string>
|
|
{
|
|
["TenantCode"] = route.TenantCode,
|
|
["ServiceName"] = route.ServiceName,
|
|
["IsGlobal"] = route.IsGlobal.ToString()
|
|
},
|
|
Transforms = ParseTransforms(route.Transforms)
|
|
};
|
|
}
|
|
|
|
private static IReadOnlyList<RouteHeader>? ParseHeaderMatch(string? headersJson)
|
|
{
|
|
if (string.IsNullOrEmpty(headersJson)) return null;
|
|
|
|
// 解析 JSON 格式的 Header 匹配规则
|
|
// [{"Name":"X-Custom","Values":["value1"],"Mode":"ExactHeader"}]
|
|
return JsonSerializer.Deserialize<List<RouteHeader>>(headersJson);
|
|
}
|
|
|
|
private static IReadOnlyList<IReadOnlyDictionary<string, string>>? ParseTransforms(string? transformsJson)
|
|
{
|
|
if (string.IsNullOrEmpty(transformsJson)) return null;
|
|
|
|
// 解析 JSON 格式的转换规则
|
|
return JsonSerializer.Deserialize<List<Dictionary<string, string>>>(transformsJson);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. IClusterStore 接口设计
|
|
|
|
```csharp
|
|
public interface IClusterStore
|
|
{
|
|
// 基础查询
|
|
Task<GwCluster?> FindByIdAsync(string id, CancellationToken cancellationToken = default);
|
|
Task<GwCluster?> FindByClusterIdAsync(string clusterId, CancellationToken cancellationToken = default);
|
|
Task<IList<GwCluster>> GetAllAsync(CancellationToken cancellationToken = default);
|
|
|
|
// 分页查询
|
|
Task<IList<GwCluster>> GetPagedAsync(
|
|
int page,
|
|
int pageSize,
|
|
string? name = null,
|
|
ClusterStatus? status = null,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
Task<int> GetCountAsync(
|
|
string? name = null,
|
|
ClusterStatus? status = null,
|
|
CancellationToken cancellationToken = default);
|
|
|
|
// CRUD 操作
|
|
Task<IdentityResult> CreateAsync(GwCluster cluster, CancellationToken cancellationToken = default);
|
|
Task<IdentityResult> UpdateAsync(GwCluster cluster, CancellationToken cancellationToken = default);
|
|
Task<IdentityResult> DeleteAsync(GwCluster cluster, CancellationToken cancellationToken = default);
|
|
|
|
// Destination 管理
|
|
Task<IdentityResult> AddDestinationAsync(string clusterId, GwDestination destination, CancellationToken cancellationToken = default);
|
|
Task<IdentityResult> UpdateDestinationAsync(string clusterId, GwDestination destination, CancellationToken cancellationToken = default);
|
|
Task<IdentityResult> RemoveDestinationAsync(string clusterId, string destinationId, CancellationToken cancellationToken = default);
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 6. 会话亲和实现
|
|
|
|
### 中间件:设置会话键
|
|
|
|
```csharp
|
|
public class SessionAffinityMiddleware
|
|
{
|
|
private readonly RequestDelegate _next;
|
|
|
|
public SessionAffinityMiddleware(RequestDelegate next)
|
|
{
|
|
_next = next;
|
|
}
|
|
|
|
public async Task InvokeAsync(HttpContext context)
|
|
{
|
|
// 优先使用 UserId
|
|
var userId = context.User?.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
|
|
|
// 其次使用 TenantCode
|
|
var tenantCode = context.User?.FindFirst("TenantCode")?.Value
|
|
?? context.Request.Headers["X-Tenant-Code"].FirstOrDefault();
|
|
|
|
// 设置会话亲和键
|
|
var sessionKey = userId ?? tenantCode ?? "anonymous";
|
|
context.Items["SessionAffinityKey"] = sessionKey;
|
|
|
|
// 添加到请求头供 YARP 使用
|
|
context.Request.Headers["X-Session-Key"] = sessionKey;
|
|
|
|
await _next(context);
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 7. 依赖关系图
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Wave 1 │
|
|
│ ┌─────────────────┐ ┌─────────────────┐ │
|
|
│ │ GwCluster.cs │ │ GwTenantRoute │ │
|
|
│ │ GwDestination │ │ (扩展字段) │ │
|
|
│ │ 值对象 │ │ │ │
|
|
│ └────────┬────────┘ └────────┬────────┘ │
|
|
│ │ │ │
|
|
└───────────┼────────────────────┼────────────────────────────┘
|
|
│ │
|
|
▼ ▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Wave 2 │
|
|
│ ┌─────────────────────────────────────────────────────┐ │
|
|
│ │ PlatformDbContext.cs │ │
|
|
│ │ - 移除 GwTenant, GwServiceInstance DbSet │ │
|
|
│ │ - 添加 GwCluster DbSet │ │
|
|
│ │ - 配置 Owned Entities │ │
|
|
│ └─────────────────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
│
|
|
▼
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
│ Wave 3 │
|
|
│ ┌─────────────────┐ ┌─────────────────┐ │
|
|
│ │ IClusterStore │ │ IRouteStore │ │
|
|
│ │ ClusterStore │ │ RouteStore │ │
|
|
│ │ (替换Instance) │ │ (更新) │ │
|
|
│ └────────┬────────┘ └────────┬────────┘ │
|
|
│ │ │ │
|
|
│ ▼ ▼ │
|
|
│ ┌─────────────────────────────────────────────────────┐ │
|
|
│ │ Extensions.cs │ │
|
|
│ │ - 移除 IInstanceStore 注册 │ │
|
|
│ │ - 添加 IClusterStore 注册 │ │
|
|
│ └─────────────────────────────────────────────────────┘ │
|
|
└─────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## 8. 风险评估
|
|
|
|
| 风险 | 影响 | 缓解措施 |
|
|
|------|------|----------|
|
|
| 删除 GwTenant 导致数据丢失 | 高 | 确认无数据后再删除,或提供迁移脚本 |
|
|
| 删除 GwServiceInstance 导致数据丢失 | 高 | 同上 |
|
|
| EF Core 迁移失败 | 中 | 手动编写 SQL 迁移脚本 |
|
|
| Owned Entity 查询性能 | 低 | EF Core 8+ 已优化 |
|
|
|
|
---
|
|
|
|
## 9. 参考资源
|
|
|
|
- [EF Core Owned Entities](https://learn.microsoft.com/ef/core/modeling/owned-entities)
|
|
- [YARP Configuration](https://microsoft.github.io/reverse-proxy/)
|
|
- [YARP GitHub](https://github.com/microsoft/reverse-proxy)
|
|
- 项目文档: `docs/yarp-configuration-model.md`
|
|
|
|
---
|
|
|
|
*研究完成日期: 2026-03-03* |