EF Core 8多项目架构下TPH继承迁移错误及关系设计疑问
EF Core 8 TPH 继承下迁移失败问题及设计疑问
在多项目架构中开发,模型与数据访问层位于独立的.csproj类库文件中。使用EF Core 8和TPH(表每层次结构)继承时,在VS2022执行Add-Migration命令遇到如下错误:
无法创建类型为“ContentDbContext”的“DbContext”。创建实例时引发异常:“无法将集合导航属性‘Paragraphs’添加到实体类型‘Summary’,因为其CLR类型‘ICollection’未实现‘IEnumerable<>’。集合导航属性必须实现相关实体的‘IEnumerable<>’。”
该问题疑似与TPH配置相关,以下是代码结构概述:
相关代码
模型接口
public interface IParagraph { string Content { get; set; } } public interface IHasParagraphs<T> where T : IParagraph { ICollection<T> Paragraphs { get; set; } }
抽象基类
public abstract class ParagraphBase : TopicStructureBase, IOrderable, IParagraph { public enum ParagraphParentType { ParagraphBlock, Summary } public ParagraphParentType? ParentType { get; set; } public int DisplayOrder { get; set; } public string Content { get; set; } = string.Empty; }
派生类
public class SummaryParagraph : ParagraphBase { public Guid SummaryId { get; set; } public virtual Summary Summary { get; set; } = new(); } public class ContentParagraph : ParagraphBase { public Guid ParagraphBlockId { get; set; } public virtual ParagraphBlock Block { get; set; } = new(); }
聚合根
public class Summary : IdentifiableEntityBase, IHasParagraphs<SummaryParagraph> { public virtual ICollection<SummaryParagraph> Paragraphs { get; set; } = new HashSet<SummaryParagraph>(); } public class ParagraphBlock : TopicElementBase, IHasParagraphs<ContentParagraph> { public virtual ICollection<ContentParagraph> Paragraphs { get; set; } = new HashSet<ContentParagraph>(); }
实体配置
public void Configure(EntityTypeBuilder<ParagraphBase> builder) { builder.HasDiscriminator(p => p.ParentType) .HasValue<ContentParagraph>(ParagraphParentType.ParagraphBlock) .HasValue<SummaryParagraph>(ParagraphParentType.Summary); builder.HasOne<Summary>() .WithMany(s => s.Paragraphs) .HasForeignKey(sp => ((SummaryParagraph)sp).SummaryId); builder.HasOne<ParagraphBlock>() .WithMany(pb => pb.Paragraphs) .HasForeignKey(cp => ((ContentParagraph)cp).ParagraphBlockId); }
已尝试方案
- 将外键属性(
SummaryId、ParagraphBlockId)及导航属性移至基类:此操作解决了错误,但破坏了特性化设计,强制所有派生类共享这些属性。
疑问
外键属性设计:按逻辑ParagraphBase不应知晓实体关系,仅派生类(SummaryParagraph和ContentParagraph)应了解各自的容器(Summary和ParagraphBlock),并拥有专用集合(ICollection<SummaryParagraph>和ICollection<ContentParagraph>)。但EF Core似乎要求基类知晓关系,与预期设计冲突。是否应由派生类管理关系?若EF Core坚持基类知晓关系,原因是什么?
可正常运行的旧版本代码
模型接口
public interface IParent { Guid ParentId { get; set; } } public interface IAlineasContainer<T> where T : IAlinea { ICollection<T> Alineas { get; set; } } public interface IAlinea { string Content { get; set; } Guid? ReplacedById { get; set; } Guid? ReplacesId { get; set; } }
抽象基类
public abstract class AlineaBase : TopicBase, IUtoYearDisplay, IOrderedDisplay, IAlinea { public enum AlineaParentType { Paragraph, Summary } public string Content { get; set; } public Guid? ReplacedById { get; set; } public virtual AlineaBase? ReplacedBy { get; set; } public Guid? ReplacesId { get; set; } public virtual AlineaBase? Replaces { get; set; } public int? DisplayEndYearUto { get; set; } public int? DisplayStartYearUto { get; set; } public int DisplayOrder { get; set; } public AlineaParentType ParentType { get; set; } public virtual object Parent { get; set; } }
派生类
public class ParagraphAlinea : AlineaBase { public Guid ParagraphId { get; set; } public virtual Paragraph Parent { get; set; } } public class SummaryAlinea : AlineaBase { public Guid SummaryId { get; set; } public virtual Summary Parent { get; set; } }
聚合根
public class Paragraph : IllustratedTopicSubdivisionBase, IAlineasContainer<ParagraphAlinea>, IDraftable { public bool IsDraft { get; set; } = true; public virtual Article Parent { get; set; } public virtual ICollection<ParagraphAlinea> Alineas { get; set; } } public class Summary : IdentifierModelBase, IAlineasContainer<SummaryAlinea>, IParent, IDraftable, IIllustratable { public Guid ParentId { get; set; } public bool IsDraft { get; set; } = true; public virtual Topic Parent { get; set; } public virtual ICollection<SummaryAlinea> Alineas { get; set; } public Guid? IllustrationAssociationId { get; set; } public virtual IllustrationAssociation? Illustration { get; set; } }
实体配置
private void ConfigureEntities(ModelBuilder modelBuilder) { //Other code ConfigureAlineaTablePerHierarchy(modelBuilder); ConfigureZeroOrOneToManyRelationships(modelBuilder); ConfigureOneToOneRelationships(modelBuilder); } private void ConfigureAlineaTablePerHierarchy(ModelBuilder modelBuilder) { modelBuilder.Entity<AlineaBase>() .HasDiscriminator(x => x.ParentType) .HasValue<SummaryAlinea>(AlineaParentType.Summary) .HasValue<ParagraphAlinea>(AlineaParentType.Paragraph); modelBuilder.Entity<SummaryAlinea>() .Property(sa => sa.SummaryId) .HasColumnName("SummaryId"); modelBuilder.Entity<ParagraphAlinea>() .Property(pa => pa.ParagraphId) .HasColumnName("ParagraphId"); } private void ConfigureZeroOrOneToManyRelationships(ModelBuilder modelBuilder) { // other code ConfigureZeroOrOneToMany<Summary, SummaryAlinea>(modelBuilder, a => a.Parent, s => s.Alineas, a => a.SummaryId); ConfigureZeroOrOneToMany<Paragraph, ParagraphAlinea>(modelBuilder, a => a.Parent, p => p.Alineas, a => a.ParagraphId); void ConfigureZeroOrOneToMany<TParent, TChild>(ModelBuilder modelBuilder, Expression<Func<TChild, TParent>> parentProperty, Expression<Func<TParent, IEnumerable<TChild>>> childrenProperty, Expression<Func<TChild, object>> foreignKeyProperty, DeleteBehavior deleteBehavior = DeleteBehavior.Restrict) where TParent : class where TChild : class { modelBuilder.Entity<TChild>() .HasOne(parentProperty) .WithMany(childrenProperty) .HasForeignKey(foreignKeyProperty) .OnDelete(deleteBehavior); } }
仅修改了实体名称,此版本可正常运行。
新旧版本命名映射
| 旧版本 | 新版本 |
|---|---|
IAlinea | IParagraph |
IAlineasContainer<T> | IHasParagraphs<T> |
AlineaBase | ParagraphBase |
ParagraphAlinea | SummaryParagraph |
SummaryAlinea | ContentParagraph |
Paragraph | ParagraphBlock |
Summary | Summary |
Alineas(Paragraph和Summary中) | Paragraphs(ParagraphBlock和Summary中) |
AlineaParentType | ParagraphParentType |
内容的提问来源于stack exchange,提问作者Acetone9177
相关产品推荐
相关产品推荐

