Some checks are pending
Publish Platform NuGet Packages / build (push) Waiting to run
修复在 EF Core 10 中使用 JSON 值对象时出现的映射错误:
## 问题
在 EF Core 10 中,GwRouteMatch 类的嵌套集合属性(Headers 和 QueryParameters)
导致 "Unable to determine the relationship" 错误。
## 解决方案
1. 在 PlatformDbContext 中使用 modelBuilder.Ignore<> 忽略相关类型
2. 将 OwnsOne().ToJson() 配置改为使用值转换器(Value Converter)
将对象序列化为 JSON 字符串存储到 jsonb 列
3. 在 GwRouteMatch 类的 Headers 和 QueryParameters 属性上添加 [NotMapped] 特性
4. 添加 [JsonInclude] 特性确保序列化包含这些属性
## 技术细节
- 使用 HasColumnType("jsonb") 存储 JSON 数据
- 使用值转换器处理对象序列化/反序列化
- 保持与 PostgreSQL jsonb 类型的兼容性
## 文件变更
- 修改: Fengling.Platform.Domain/AggregatesModel/GatewayAggregate/GwRouteMatch.cs
- 修改: Fengling.Platform.Infrastructure/PlatformDbContext.cs
关联任务: IMPL-4 (EF Core 兼容性修复)
关联重构计划: WFS-gateway-refactor
188 lines
8.5 KiB
C#
188 lines
8.5 KiB
C#
using Fengling.Platform.Domain.AggregatesModel.GatewayAggregate;
|
||
using Fengling.Platform.Domain.AggregatesModel.RoleAggregate;
|
||
using Fengling.Platform.Domain.AggregatesModel.TenantAggregate;
|
||
using Fengling.Platform.Domain.AggregatesModel.UserAggregate;
|
||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||
using Microsoft.EntityFrameworkCore;
|
||
using Microsoft.EntityFrameworkCore.ChangeTracking;
|
||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||
using System.Text.Json;
|
||
|
||
namespace Fengling.Platform.Infrastructure;
|
||
|
||
public class PlatformDbContext(DbContextOptions options)
|
||
: IdentityDbContext<ApplicationUser, ApplicationRole, long>(options)
|
||
{
|
||
public DbSet<Tenant> Tenants => Set<Tenant>();
|
||
public DbSet<AccessLog> AccessLogs => Set<AccessLog>();
|
||
public DbSet<AuditLog> AuditLogs => Set<AuditLog>();
|
||
|
||
// Gateway 实体
|
||
public DbSet<GwTenantRoute> GwTenantRoutes => Set<GwTenantRoute>();
|
||
public DbSet<GwCluster> GwClusters => Set<GwCluster>();
|
||
|
||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||
{
|
||
if (modelBuilder is null)
|
||
{
|
||
throw new ArgumentNullException(nameof(modelBuilder));
|
||
}
|
||
|
||
// 忽略这些类型,让它们只作为 JSON 值对象使用
|
||
modelBuilder.Ignore<GwRouteMatch>();
|
||
modelBuilder.Ignore<GwRouteHeader>();
|
||
modelBuilder.Ignore<GwRouteQueryParameter>();
|
||
modelBuilder.Ignore<GwTransform>();
|
||
|
||
modelBuilder.Entity<ApplicationUser>(entity =>
|
||
{
|
||
entity.Property(e => e.PhoneNumber).HasMaxLength(20);
|
||
entity.HasIndex(e => e.PhoneNumber).IsUnique();
|
||
|
||
entity.OwnsOne(e => e.TenantInfo, navigationBuilder =>
|
||
{
|
||
navigationBuilder.Property(t => t.TenantCode).HasColumnName("TenantCode");
|
||
navigationBuilder.Property(t => t.TenantId).HasColumnName("TenantId");
|
||
navigationBuilder.Property(t => t.TenantName).HasColumnName("TenantName");
|
||
navigationBuilder.WithOwner();
|
||
});
|
||
});
|
||
|
||
modelBuilder.Entity<ApplicationRole>(entity =>
|
||
{
|
||
entity.Property(e => e.Description).HasMaxLength(200);
|
||
});
|
||
|
||
modelBuilder.Entity<AccessLog>(entity =>
|
||
{
|
||
entity.HasKey(e => e.Id);
|
||
entity.HasIndex(e => e.CreatedAt);
|
||
entity.HasIndex(e => e.UserName);
|
||
entity.HasIndex(e => e.TenantId);
|
||
entity.HasIndex(e => e.Action);
|
||
entity.HasIndex(e => e.Status);
|
||
entity.Property(e => e.UserName).HasMaxLength(50);
|
||
entity.Property(e => e.TenantId).HasMaxLength(50);
|
||
entity.Property(e => e.Action).HasMaxLength(20);
|
||
entity.Property(e => e.Resource).HasMaxLength(200);
|
||
entity.Property(e => e.Method).HasMaxLength(10);
|
||
entity.Property(e => e.IpAddress).HasMaxLength(50);
|
||
entity.Property(e => e.UserAgent).HasMaxLength(500);
|
||
entity.Property(e => e.Status).HasMaxLength(20);
|
||
});
|
||
|
||
modelBuilder.Entity<AuditLog>(entity =>
|
||
{
|
||
entity.HasKey(e => e.Id);
|
||
entity.HasIndex(e => e.CreatedAt);
|
||
entity.HasIndex(e => e.Operator);
|
||
entity.HasIndex(e => e.TenantId);
|
||
entity.HasIndex(e => e.Operation);
|
||
entity.HasIndex(e => e.Action);
|
||
entity.Property(e => e.Operator).HasMaxLength(50);
|
||
entity.Property(e => e.TenantId).HasMaxLength(50);
|
||
entity.Property(e => e.Operation).HasMaxLength(20);
|
||
entity.Property(e => e.Action).HasMaxLength(20);
|
||
entity.Property(e => e.TargetType).HasMaxLength(50);
|
||
entity.Property(e => e.TargetName).HasMaxLength(100);
|
||
entity.Property(e => e.IpAddress).HasMaxLength(50);
|
||
entity.Property(e => e.Description).HasMaxLength(500);
|
||
entity.Property(e => e.Status).HasMaxLength(20);
|
||
});
|
||
|
||
// Gateway 实体配置
|
||
modelBuilder.Entity<GwTenantRoute>(entity =>
|
||
{
|
||
entity.HasKey(e => e.Id);
|
||
entity.Property(e => e.TenantCode).HasMaxLength(50);
|
||
entity.Property(e => e.ServiceName).HasMaxLength(100).IsRequired();
|
||
entity.Property(e => e.ClusterId).HasMaxLength(100).IsRequired();
|
||
entity.Property(e => e.AuthorizationPolicy).HasMaxLength(100);
|
||
entity.Property(e => e.CorsPolicy).HasMaxLength(100);
|
||
entity.Property(e => e.RateLimiterPolicy).HasMaxLength(100);
|
||
|
||
// 枚举转换为字符串
|
||
entity.Property(e => e.LoadBalancingPolicy)
|
||
.HasConversion(
|
||
v => v.HasValue ? v.Value.ToString() : null,
|
||
v => v != null ? Enum.Parse<GwLoadBalancingPolicy>(v) : null
|
||
)
|
||
.HasMaxLength(50);
|
||
|
||
// 值对象映射为 JSON 列 - 使用值转换器
|
||
var jsonOptions = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase };
|
||
entity.Property(e => e.Match)
|
||
.HasConversion(
|
||
v => JsonSerializer.Serialize(v, jsonOptions),
|
||
v => JsonSerializer.Deserialize<GwRouteMatch>(v, jsonOptions)!,
|
||
new ValueComparer<GwRouteMatch>(
|
||
(c1, c2) => JsonSerializer.Serialize(c1, jsonOptions) == JsonSerializer.Serialize(c2, jsonOptions),
|
||
c => c == null ? 0 : JsonSerializer.Serialize(c, jsonOptions).GetHashCode(),
|
||
c => JsonSerializer.Deserialize<GwRouteMatch>(JsonSerializer.Serialize(c, jsonOptions), jsonOptions)!))
|
||
.HasColumnType("jsonb");
|
||
|
||
// 转换规则映射为 JSON 列 - 使用值转换器
|
||
entity.Property(e => e.Transforms)
|
||
.HasConversion(
|
||
v => JsonSerializer.Serialize(v, jsonOptions),
|
||
v => JsonSerializer.Deserialize<List<GwTransform>>(v, jsonOptions),
|
||
new ValueComparer<List<GwTransform>>(
|
||
(c1, c2) => JsonSerializer.Serialize(c1, jsonOptions) == JsonSerializer.Serialize(c2, jsonOptions),
|
||
c => c == null ? 0 : JsonSerializer.Serialize(c, jsonOptions).GetHashCode(),
|
||
c => JsonSerializer.Deserialize<List<GwTransform>>(JsonSerializer.Serialize(c, jsonOptions), jsonOptions)!))
|
||
.HasColumnType("jsonb");
|
||
|
||
entity.HasIndex(e => e.TenantCode);
|
||
entity.HasIndex(e => e.ServiceName);
|
||
entity.HasIndex(e => e.ClusterId);
|
||
entity.HasIndex(e => new { e.ServiceName, e.IsGlobal, e.Status });
|
||
});
|
||
|
||
// GwCluster 聚合根配置
|
||
modelBuilder.Entity<GwCluster>(entity =>
|
||
{
|
||
entity.HasKey(e => e.Id);
|
||
entity.Property(e => e.ClusterId).HasMaxLength(100).IsRequired();
|
||
entity.Property(e => e.Name).HasMaxLength(100).IsRequired();
|
||
entity.Property(e => e.Description).HasMaxLength(500);
|
||
|
||
// 枚举转换为字符串
|
||
entity.Property(e => e.LoadBalancingPolicy)
|
||
.HasConversion(
|
||
v => v.ToString(),
|
||
v => Enum.Parse<GwLoadBalancingPolicy>(v)
|
||
)
|
||
.HasMaxLength(50);
|
||
|
||
entity.HasIndex(e => e.ClusterId).IsUnique();
|
||
entity.HasIndex(e => e.Name);
|
||
entity.HasIndex(e => e.Status);
|
||
|
||
// 配置内嵌的目标端点列表
|
||
entity.OwnsMany(e => e.Destinations, owned =>
|
||
{
|
||
owned.WithOwner().HasForeignKey("ClusterId");
|
||
owned.Property<string>("ClusterId").HasMaxLength(100);
|
||
owned.Property(d => d.DestinationId).HasMaxLength(100).IsRequired();
|
||
owned.Property(d => d.Address).HasMaxLength(200).IsRequired();
|
||
owned.Property(d => d.Health).HasMaxLength(200);
|
||
owned.HasIndex("ClusterId", "DestinationId");
|
||
});
|
||
|
||
// 配置内嵌健康检查配置(JSON 列)
|
||
entity.OwnsOne(e => e.HealthCheck, owned =>
|
||
{
|
||
owned.ToJson();
|
||
});
|
||
|
||
// 配置内嵌会话亲和配置(JSON 列)
|
||
entity.OwnsOne(e => e.SessionAffinity, owned =>
|
||
{
|
||
owned.ToJson();
|
||
});
|
||
});
|
||
|
||
modelBuilder.ApplyConfigurationsFromAssembly(typeof(PlatformDbContext).Assembly);
|
||
base.OnModelCreating(modelBuilder);
|
||
}
|
||
} |