3

So here is my issue...what I am trying to create is a self-referencing many to many relationship. Basically here is my model.

public class InformationSystem
{
  public InformationSystem()
  {
     Systems = new HashSet<InformationSystem>();
     ParentSystems = new HashSet<InformationSystem>();
  }
 [Key()]
 public int InformationSystemID { get; set; }
 public string InformationSystemName { get; set; }
 //Navigation properties
 public virtual ICollection<InformationSystem> Systems { get; set; }
 public virtual ICollection<InformationSystem> ParentSystems { get; set; }
}

The idea being that a system can have many parents and a parent can have many children. I know how to do a self-referencing entity where many children can have one parent. What is tripping me up is the many to many part. Below is my DbContext.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
  modelBuilder.Entity<InformationSystem>(entity =>
  {
    entity
       .HasMany(e => e.ParentSystems)
       .WithMany(e => e.Systems)
       .OnDelete(DeleteBehavior.Restrict);
  });

However on my DbContext, I get an error that .WithMany does not contain a definition for with many that would accept an input of type collection. I know that basically what needs to be built is a link table when the code first creates a migration and updates the database. The link table I think would have two columns and no key. One column would be InformationSystemID and one would be ParentInformationSystemID. Both would be foreign keys. I also know that for this to work right, the delete behavior should be restrict so that if an entry is deleted or updated in the link table, that change won't cascade (and create a loop). Can someone please point me in the right direction of what I need to do to get EF Core 3 to do this correctly? If I had to create a link table myself, how would I go about doing that? And what would I need to do in my DbContext? I know that a link table would look something like this:
I'd greatly appreciate it.

public class InfoSysToParentInfoSys
{
  public int InfoSysID;
  public virtual InformationSystem InfoSys;

  public int ParentInfoSysID;
  public virtual InformationSystem ParentInfoSys;
}
  • Many to many relationships without a link table are not supported even in EF Core 3 https://github.com/dotnet/efcore/issues/1368 – milo Apr 10 '20 at 06:21
  • I understand they wouldn't be. From what I've read, Entity Framework Code First would actually create that link table when it creates the migration. Running that migration with Update-Database would create the table and relationships. EF Core would create the link table based on what I define in Fluent API. The issue I am having is that .WithMany method. If I understand correctly, you could also tell Fluent API what your link table should be. – Chris Kuznicki Apr 10 '20 at 06:27
  • If I understand the article correctly, to create a join table myself I could do something like this `public class InfoSysToParentInfoSys { public int InfoSysID; public virtual InformationSystem InfoSys; public int ParentInfoSysID; public virtual InformationSystem ParentInfoSys; }` – Chris Kuznicki Apr 10 '20 at 06:33

1 Answers1

5

In EF Core it is necessary to include an entity in the model to represent the join table in the M:N relation, and then add navigation properties to either side of the many-to-many relations that point to the join entity.

The new tableS:

public class InformationSystem
{
    public InformationSystem()
    {
    }

    [Key()]
    public virtual int InformationSystemID { get; set; }
    public virtual string InformationSystemName { get; set; }

    public virtual ICollection<InformationSystemRelation> Systems { get; set; }
    public virtual ICollection<InformationSystemRelation> ParentSystems { get; set; }

}


public class InformationSystemRelation
{
    public int ParentId { get; set; }
    public InformationSystem Parent { get; set; }

    public int ChildId { get; set; }
    public InformationSystem Child { get; set; }
}

The mapping:

modelBuilder.Entity<InformationSystemRelation>()
    .HasKey(x => new { x.ParentId, x.ChildId });

modelBuilder.Entity<InformationSystemRelation>()
    .HasOne(x => x.Parent)
    .WithMany(x => x.Systems)
    .HasForeignKey(x => x.ParentId)                    
    .OnDelete(DeleteBehavior.Restrict);


modelBuilder.Entity<InformationSystemRelation>()
    .HasOne(x => x.Child)
    .WithMany(x => x.ParentSystems)
    .HasForeignKey(x => x.ChildId)
    .OnDelete(DeleteBehavior.Restrict);

The whole sample:

class Program
{
    static void Main(string[] args)
    {
        var db = new MyDbContext();

        var is1 = new InformationSystem() { InformationSystemName = "is1" };
        var is2 = new InformationSystem() { InformationSystemName = "is2" };
        var is3 = new InformationSystem() { InformationSystemName = "is3" };
        var is4 = new InformationSystem() { InformationSystemName = "is4" };

        db.InformationSystems.Add(is1);
        db.InformationSystems.Add(is2);
        db.InformationSystems.Add(is3);
        db.InformationSystems.Add(is4);

        db.SaveChanges();

        var r1 = new InformationSystemRelation() { ParentId = 1, ChildId = 2 };
        var r2 = new InformationSystemRelation() { ParentId = 1, ChildId = 3 };
        var r3 = new InformationSystemRelation() { ParentId = 4, ChildId = 2 };
        var r4 = new InformationSystemRelation() { ParentId = 2, ChildId = 3 };
        var r5 = new InformationSystemRelation() { ParentId = 2, ChildId = 4 };

        db.InformationSystemRelations.Add(r1);
        db.InformationSystemRelations.Add(r2);
        db.InformationSystemRelations.Add(r3);
        db.InformationSystemRelations.Add(r4);
        db.InformationSystemRelations.Add(r5);

        db.SaveChanges();

        var o2 = db.InformationSystems.Include(x => x.Systems).Include(x => x.ParentSystems).Single(x => x.InformationSystemID == 2);
    }

}

public class InformationSystem
{
    public InformationSystem()
    {
    }

    [Key()]
    public virtual int InformationSystemID { get; set; }
    public virtual string InformationSystemName { get; set; }

    public virtual ICollection<InformationSystemRelation> Systems { get; set; }
    public virtual ICollection<InformationSystemRelation> ParentSystems { get; set; }

}

public class MyDbContext : DbContext
{

    public DbSet<InformationSystem> InformationSystems { get; set; }
    public DbSet<InformationSystemRelation> InformationSystemRelations { get; set; }


    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<InformationSystem>(entity =>
        {
            modelBuilder.Entity<InformationSystemRelation>()
                    .HasKey(x => new { x.ParentId, x.ChildId });

            modelBuilder.Entity<InformationSystemRelation>()
                .HasOne(x => x.Parent)
                .WithMany(x => x.Systems)
                .HasForeignKey(x => x.ParentId)
                .OnDelete(DeleteBehavior.Restrict);

            modelBuilder.Entity<InformationSystemRelation>()
                .HasOne(x => x.Child)
                .WithMany(x => x.ParentSystems)
                .HasForeignKey(x => x.ChildId)
                .OnDelete(DeleteBehavior.Restrict);
        });
    }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder.UseSqlServer("data source=(local)\\SQLEXPRESS;Initial catalog=Test;Integrated security=SSPI");
        base.OnConfiguring(optionsBuilder);
    }
}

public class InformationSystemRelation
{
    public int ParentId { get; set; }
    public InformationSystem Parent { get; set; }

    public int ChildId { get; set; }
    public InformationSystem Child { get; set; }
}
Martin Staufcik
  • 8,295
  • 4
  • 44
  • 63
  • That is very close but I get an error that there needs to be an explicit cast – Chris Kuznicki Apr 10 '20 at 06:59
  • The .WithMany specifically under x.Systems and x.ParentSystems – Chris Kuznicki Apr 10 '20 at 07:15
  • added the InformationSystem class, it has removed the collections from the constructor – Martin Staufcik Apr 10 '20 at 07:18
  • Ok...the error in explicit cast went away and was able to create a migration...however now I can't update the database because it is complaining about the ON DELETE CASCADE on the Foreign Key. I thought the .OnDelete with a DeleteBehavior of Restrict took care of that. Looking at the migration code in the Up method when it creates the Foreign Keys, the onDelete property is set to ReferentialAction.Restrict. What could I be doing wrong? – Chris Kuznicki Apr 10 '20 at 08:01
  • Yes, `.OnDelete(DeleteBehavior.Restrict)` should take care of that. what might help, is to delete the migration without this statement and create a new migration – Martin Staufcik Apr 10 '20 at 08:08
  • Won't let me move discussion to chat...but tried removing the .OnDelete and creating a migration. Migration created two Foreign Keys with .OnDelete of cascade. Update-Database failed expectedly. Not sure how to proceed. – Chris Kuznicki Apr 10 '20 at 08:17
  • Interestingly, when I go to Update-Database with the migration that has the .OnDelete set to Restrict, the SQL that is created is ON DELETE CASCADE which makes no sense. – Chris Kuznicki Apr 10 '20 at 08:25
  • tried to reproduce, but could not, for me it works, the generated sql for the foreign keys does not have the ON CASCASE DELETE, added the whole sample to the answer – Martin Staufcik Apr 10 '20 at 08:39
  • Thank you very much for the sample. I will create a console app and give it a try. I'm wondering if I just need to delete the database all together to get it to work right. There's no info in the database so nothing to lose. – Chris Kuznicki Apr 10 '20 at 08:45
  • So I tried creating the sample application exactly as you provided. I am using NuGet packages for EntityFrameworkCore 3.0, .Design 3.0, and .SqlServer 3.0. But it still is creating SQL statements of ON CASCADE DELETE. If I could post a screen shot I would. This is being really stubborn and I really don't have any idea how to fix this. – Chris Kuznicki Apr 10 '20 at 09:56
  • Plus the console application keeps bombing out complaining about keyword not supported for @Data Source – Chris Kuznicki Apr 10 '20 at 10:09
  • The error I am getting is Cannot open database...login failed. I am using the localdb for the database engine. I have to troubleshoot some more but I am at a loss. Please guide me as I am at a loss. – Chris Kuznicki Apr 10 '20 at 10:49