Configure/Build Table that holds two Ids pointing to the same entity with ef core

264 Views Asked by At

I'm struggling with building and configuring a table with ef core.

I have/want an entity/table that shows a relationship between two entities.

So I have Book2 that is a sequel of Book1, and Book2 shares same characters with Book3 and so on.

I had the simple idea to use the model below that just holds the IDs of the two items and then has properties that define the relationship and an optional description.

public class MangaRelation : BaseEntity
{
    public int ParentId { get; set; }
    public int ChildId { get; set; }

    public virtual Manga? Parent { get; set; }
    public virtual Manga? Child { get; set; }

    public RelationType RelationType { get; set; }
    public string? Description { get; set; }
}

Also I have another Model where I want to have recommendation, where the concept is similar in the way that we also have two ids pointing to the same table.

Book2 is recommended for Book1 and then a reason why User xy thinks Book2 should be recommended if you read Book1.

The Model looks like this:

public class MangaRecommendation : BaseEntity
{
    public int MangaId { get; set; }
    public int RecommendedMangaId { get; set; }
    public virtual Manga? Manga { get; set; }
    public virtual Manga? RecommendedManga { get; set; }
    public string? Reason { get; set; }
}

I shortened the model to show just the properties that are related to this problem.

public partial class Manga : BaseEntity
{
    public int MangaId { get; set; }
    public virtual ICollection<MangaRelation> Relations { get; set; } = new HashSet<MangaRelation>();
    public virtual ICollection<MangaRecommendation> Recommendations { get; set; } = new HashSet<MangaRecommendation>();
}

In the DbContext I configured the tables like this:

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        RegisterDbFunctions(modelBuilder);
        modelBuilder.Entity<Manga>(e =>
        {
            e.HasKey(p => p.MangaId);
            e.HasMany(p => p.RelationShips).WithOne(m => m.Parent).OnDelete(DeleteBehavior.Cascade);
            e.HasMany(p => p.Recommendations).WithOne(m => m.Manga).OnDelete(DeleteBehavior.Cascade);
        });       

 modelBuilder.Entity<MangaRelation>(e =>
        {
            e.HasKey(k => new { k.MangaParentId, k.MangaChildId });
            e.HasIndex(p => p.MangaParentId);
            e.HasIndex(p => p.MangaChildId);
            e.Property(p => p.MangaParentId);
            e.Property(p => p.MangaChildId);
            e.HasOne(p => p.Parent).WithMany().HasForeignKey(k => k.MangaParentId).HasConstraintName("FK_Manga_Parent")
                .OnDelete(DeleteBehavior.NoAction);
            e.HasOne(p => p.Child).WithMany().HasForeignKey(k => k.MangaChildId).HasConstraintName("FK_Manga_Child")
                .OnDelete(DeleteBehavior.NoAction);
            e.Property(p => p.RelationType).HasConversion<int>();
            e.Property(p => p.Description);
        });

        modelBuilder.Entity<MangaRecommondation>(e =>
        {
            e.HasKey(k => new { k.MangaId, k.RecommendedMangaId });
            e.HasIndex(p => p.MangaId);
            e.HasIndex(p => p.RecommendedMangaId);
            e.Property(p => p.Reason);
            e.Property(p => p.MangaId);
            e.HasOne(p => p.Manga).WithMany().HasForeignKey(k => k.MangaId).HasConstraintName("FK_MangaParent_Rec")
                .OnDelete(DeleteBehavior.NoAction);
            e.HasOne(p => p.RecommendedManga).WithMany().HasForeignKey(k => k.RecommendedMangaId)
                .HasConstraintName("FK_MangaRecParent_Rec").OnDelete(DeleteBehavior.NoAction);
            e.Property(p => p.RecommendedMangaId);
        });
}

If I use the ef core tool to create the migration files it says that relation couldn't be configured by convention and the tool then executes with errors and gives this weird stacktrace with this weird message (Nullable Context is enabled for all projects):

System.InvalidOperationException: Cannot scaffold C# literals of type 'System.Reflection.NullabilityInfoContext'. The provider should implement CoreTypeMapping.GenerateCodeLiteral to support using it at design time.
   at Microsoft.EntityFrameworkCore.Design.Internal.CSharpHelper.UnknownLiteral(Object value)
   at Microsoft.EntityFrameworkCore.Design.Internal.CSharpHelper.<Fragment>g__AppendMethodCall|52_0(IMethodCallCodeFragment current, <>c__DisplayClass52_0&)
   at Microsoft.EntityFrameworkCore.Design.Internal.CSharpHelper.Fragment(IMethodCallCodeFragment fragment, Int32 indent)
   at Microsoft.EntityFrameworkCore.Migrations.Design.CSharpSnapshotGenerator.GenerateAnnotations(String builderName, IAnnotatable annotatable, IndentedStringBuilder stringBuilder, Dictionary`2 annotations, Boolean inChainedCall, Boolean leadingNewline)
   at Microsoft.EntityFrameworkCore.Migrations.Design.CSharpSnapshotGenerator.Generate(String modelBuilderName, IModel model, IndentedStringBuilder stringBuilder)
   at Microsoft.EntityFrameworkCore.Migrations.Design.CSharpMigrationsGenerator.GenerateMetadata(String migrationNamespace, Type contextType, String migrationName, String migrationId, IModel targetModel)
   at Microsoft.EntityFrameworkCore.Migrations.Design.MigrationsScaffolder.ScaffoldMigration(String migrationName, String rootNamespace, String subNamespace, String language)
   at Microsoft.EntityFrameworkCore.Design.Internal.MigrationsOperations.AddMigration(String name, String outputDir, String contextType, String namespace)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigrationImpl(String name, String outputDir, String contextType, String namespace)
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.AddMigration.<>c__DisplayClass0_0.<.ctor>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.<>c__DisplayClass3_0`1.<Execute>b__0()
   at Microsoft.EntityFrameworkCore.Design.OperationExecutor.OperationBase.Execute(Action action)
Cannot scaffold C# literals of type 'System.Reflection.NullabilityInfoContext'. The provider should implement CoreTypeMapping.GenerateCodeLiteral to support using it at design time.
1

There are 1 best solutions below

1
On

Not directly an answer, but it seems that Manga properties should not be marked as nullable (i.e. Manga?). I would argue that having records in those tables would have sense only if both relations are present. Also it contradicts nullability of the Id's (Parent/Child, Manga/RecommendedManga, which are not nullable) properties:

public class MangaRelation : BaseEntity
{
    public int ParentId { get; set; }
    public int ChildId { get; set; }

    public virtual Manga Parent { get; set; }
    public virtual Manga Child { get; set; }

    public RelationType RelationType { get; set; }
    public string? Description { get; set; }
}

public class MangaRecommendation : BaseEntity
{
    public int MangaId { get; set; }
    public int RecommendedMangaId { get; set; }
    public virtual Manga Manga { get; set; }
    public virtual Manga RecommendedManga { get; set; }
    public string? Reason { get; set; }
}