You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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);
    }
}

仅修改了实体名称,此版本可正常运行。


新旧版本命名映射

旧版本新版本
IAlineaIParagraph
IAlineasContainer<T>IHasParagraphs<T>
AlineaBaseParagraphBase
ParagraphAlineaSummaryParagraph
SummaryAlineaContentParagraph
ParagraphParagraphBlock
SummarySummary
Alineas(Paragraph和Summary中)Paragraphs(ParagraphBlock和Summary中)
AlineaParentTypeParagraphParentType

内容的提问来源于stack exchange,提问作者Acetone9177

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 17:35:57