使用复合键时无法添加EF Core迁移的解决方法
我有以下实体:
public sealed class CanonicalMake : EntityBase<MakeId> { private CanonicalMake() { } public MakeId MakeId { get; private init; } protected override MakeId Id => MakeId; public string Name { get; private init; } //Other static methods Create, Validate etc. } public sealed class CanonicalModel : EntityBase<ModelId> { private CanonicalModel() { } public ModelId ModelId { get; private init; } protected override ModelId Id => ModelId; public string Name { get; private init; } //Other static methods Create, Validate etc. } public class AircraftSighting: EntityBase<AircraftSightingId> { private AircraftSighting() { } public AircraftSightingId AircraftSightingId { get; private init; } protected override AircraftSightingId Id => AircraftSightingId; // 每个目击记录可能关联0或1个标准型号 public ModelId? CanonicalModelId { get; private init; } //Other static methods Create, Validate etc. }
强类型标识符定义:
public class MakeId : IdentifierBase { public MakeId(): base() { } public MakeId(Guid guid): base(guid) { } } public class ModelId : ValueObjectBase { private ModelId() { } public Guid MakeGuid { get; private init; } public Guid ModelGuid { get; private init; } protected override IEnumerable<object> GetEqualityComponents() { yield return MakeGuid; yield return ModelGuid; } public static ModelId CreateFromMakeId(MakeId makeId, Guid modelGuid) { return new ModelId { MakeGuid = makeId.Guid, ModelGuid = modelGuid }; } public static ModelId CreateFromMakeId(MakeId makeId) { return new ModelId { MakeGuid = makeId.Guid, ModelGuid = Guid.NewGuid() }; } } public class AircraftSightingId : IdentifierBase { public AircraftSightingId() : base() { } public AircraftSightingId(Guid guid) : base(guid) { } }
实体配置代码:
public class CanonicalMakeEntityConfig : IEntityTypeConfiguration<CanonicalMake> { public void Configure(EntityTypeBuilder<CanonicalMake> builder) { builder.HasKey(e => e.MakeId); builder.Property(e => e.MakeId).HasConversion( makeId => makeId.Guid, guid => new MakeId(guid)); builder.Property(e => e.Name).IsRequired(); } } public class CanonicalModelEntityConfig : IEntityTypeConfiguration<CanonicalModel> { public void Configure(EntityTypeBuilder<CanonicalModel> builder) { builder.HasKey(e => e.ModelId); builder.Property(e => e.Name).IsRequired(); } } public class AircraftEntityConfig : IEntityTypeConfiguration<AircraftSighting> { public void Configure(EntityTypeBuilder<AircraftSighting> builder) { builder.HasKey(e => e.AircraftSightingId); builder.Property(e => e.AircraftSightingId).HasConversion( id => id.Guid, value => new AircraftSightingId(value)); builder.Property(e => e.CanonicalModelId).HasConversion( modelId => new { MakeGuid = modelId!.MakeGuid, ModelGuid = modelId!.ModelGuid }, value => ModelId.CreateFromMakeId(new MakeId(value.MakeGuid), value.ModelGuid)); builder.HasOne<CanonicalModel>() .WithMany() .HasForeignKey(e => e.CanonicalModelId); //Other mappings } }
执行EF Core添加迁移时,抛出如下错误:
The '<>f__AnonymousType0<Guid, Guid>' property 'AircraftSighting.CanonicalModelId' could not be mapped because the database provider does not support this type. Consider converting the property value to a type supported by the database using a value converter. See https://aka.ms/efcore-docs-value-converters for more information. Alternately, exclude the property from the model using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'
问题原因
EF Core目前不支持把单个属性的转换拆分成多个数据库列,也不支持把多列合并成单个值对象属性。这是框架的已知限制,官方已在追踪相关需求。
临时解决办法
办法1:显式添加复合外键字段到领域模型
修改AircraftSighting,把ModelId拆成两个Guid字段存到数据库,领域层再封装成CanonicalModelId供业务使用:
public class AircraftSighting: EntityBase<AircraftSightingId> { private AircraftSighting() { } public AircraftSightingId AircraftSightingId { get; private init; } protected override AircraftSightingId Id => AircraftSightingId; // 直接映射到数据库的外键字段 public Guid? CanonicalMakeGuid { get; private init; } public Guid? CanonicalModelGuid { get; private init; } // 业务层用的ModelId,不映射数据库 [NotMapped] public ModelId? CanonicalModelId => CanonicalMakeGuid.HasValue && CanonicalModelGuid.HasValue ? ModelId.CreateFromMakeId(new MakeId(CanonicalMakeGuid.Value), CanonicalModelGuid.Value) : null; // 更新Create方法,接收ModelId并拆分存储 public static AircraftSighting Create(AircraftSightingId id, ModelId? modelId = null) { // 这里添加你的验证逻辑 return new AircraftSighting { AircraftSightingId = id, CanonicalMakeGuid = modelId?.MakeGuid, CanonicalModelGuid = modelId?.ModelGuid }; } }
然后更新实体配置:
public class AircraftEntityConfig : IEntityTypeConfiguration<AircraftSighting> { public void Configure(EntityTypeBuilder<AircraftSighting> builder) { builder.HasKey(e => e.AircraftSightingId); builder.Property(e => e.AircraftSightingId).HasConversion( id => id.Guid, value => new AircraftSightingId(value)); // 映射两个外键字段 builder.Property(e => e.CanonicalMakeGuid); builder.Property(e => e.CanonicalModelGuid); // 配置复合外键关联 builder.HasOne<CanonicalModel>() .WithMany() .HasForeignKey(e => new { e.CanonicalMakeGuid, e.CanonicalModelGuid }) .IsRequired(false); //Other mappings } }
该方案能保留EF Core的关联查询能力,缺点是领域模型要暴露数据库相关字段。
办法2:把ModelId序列化为JSON存单字段
如果对关联查询要求不高,可以把ModelId序列化成JSON字符串存在单个数据库列里:
public class AircraftEntityConfig : IEntityTypeConfiguration<AircraftSighting> { public void Configure(EntityTypeBuilder<AircraftSighting> builder) { builder.HasKey(e => e.AircraftSightingId); builder.Property(e => e.AircraftSightingId).HasConversion( id => id.Guid, value => new AircraftSightingId(value)); // 用JSON序列化做值转换 builder.Property(e => e.CanonicalModelId) .HasConversion( modelId => JsonSerializer.Serialize(modelId, JsonSerializerOptions.Default), json => string.IsNullOrEmpty(json) ? null : JsonSerializer.Deserialize<ModelId>(json, JsonSerializerOptions.Default)) .HasColumnType("nvarchar(max)"); // 不同数据库类型调整,比如PostgreSQL用jsonb // 注意:这种方式无法使用EF Core的关联查询,需自行处理关联逻辑 } }
该方案能保持领域模型整洁,但会失去EF Core的自动关联能力,查询性能也不如复合列。
总结
EF Core暂时不支持值对象作为复合外键的直接映射,上述两种方案可临时解决问题:
- 需要关联查询选办法1,牺牲一点领域模型整洁度;
- 看重领域模型干净选办法2,放弃自动关联查询。
内容的提问来源于stack exchange,提问作者Harindaka

