Search code examples
c#entity-frameworkassociationsfluent

3 way many to many with fluent api


I'd like to create database tables with a 3 way relationship using code first and the fluent api.

In my contrived example, a Team should have a unique combination of a Cat, a Dog, and a Pig. Another team could contain the same Cat and Pig, but not the same Dog and so on.

Firstly, I'd like to be able to get the teams containing a specific animal. myCat.Teams() And if possible I'd like to enforce uniqueness too.

public class Cat
{
    public int Id { get; set; }
    public virtual ICollection<Team> Teams { get; set; }
}
public class Dog
{
    public int Id { get; set; }
   public virtual ICollection<Team> Teams { get; set; }
}
public class Pig
{
    public Guid { get; set; }
   public virtual ICollection<Team> Teams { get; set; }
}

public class Team
{
    public int Id { get; set; }
    public int CatId { get; set; }
    public int DogId { get; set; }
    public Guid PigId { get; set; }

    public virtual Cat Cat {get; set;}
    public virtual Dog Dog {get; set;}
    public virtual Pig Pig {get; set;}
}

In OnModelCreating(), EntityTypeConfigurations are added for these objects (CatMap, DogMap, PigMap, TeamMap).

I've tried setting up HasMany relationships from the TeamMap class, and alternatively from the other direction. For example, in DogMap:

        HasMany(t => t.Teams)
        .WithRequired(t => t.Dog)
        .HasForeignKey(t => t.DogId);

but whenever I try to Add-Migration, I get errors like:

tSystem.Data.Entity.Edm.EdmAssociationConstraint: : The number of properties in the Dependent and Principal Roles in a relationship constraint must be identical.

How can I set up these associations correctly to achieve the two goals above? Thanks!!


Solution

  • The Team class shouldn't have its own Id, since the primary key is a combination of Cat, Dog, Pig. So, it should be something like:

    public class Team
    {
    
        public int CatId { get; set; }
    
        public int DogId { get; set; }
    
        public Guid PigId { get; set; }
    
        public virtual Cat Cat { get; set; }
        public virtual Dog Dog { get; set; }
        public virtual Pig Pig { get; set; }
    }
    

    Mapping:

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        //primary key, composed by a combination
        modelBuilder.Entity<Team>()
            .HasKey(i => new { i.CatId, i.DogId, i.PigId });
    
        modelBuilder.Entity<Team>()
            .HasRequired(i => i.Cat)
            .WithMany(i => i.Teams)
            .HasForeignKey(i => i.CatId)
            .WillCascadeOnDelete(false);
    
        modelBuilder.Entity<Team>()
            .HasRequired(i => i.Dog)
            .WithMany(i => i.Teams)
            .HasForeignKey(i => i.DogId)
            .WillCascadeOnDelete(false);
    
        modelBuilder.Entity<Team>()
            .HasRequired(i => i.Pig)
            .WithMany(i => i.Teams)
            .HasForeignKey(i => i.PigId)
            .WillCascadeOnDelete(false);
    
        base.OnModelCreating(modelBuilder);
    }
    

    Generated migration:

    CreateTable(
        "dbo.Teams",
        c => new
            {
                CatId = c.Int(nullable: false),
                DogId = c.Int(nullable: false),
                PigId = c.Guid(nullable: false),
            })
        .PrimaryKey(t => new { t.CatId, t.DogId, t.PigId })
        .ForeignKey("dbo.Cats", t => t.CatId, cascadeDelete: true)
        .ForeignKey("dbo.Dogs", t => t.DogId, cascadeDelete: true)
        .ForeignKey("dbo.Pigs", t => t.PigId, cascadeDelete: true)
        .Index(t => t.CatId)
        .Index(t => t.DogId)
        .Index(t => t.PigId);
    
    CreateTable(
        "dbo.Cats",
        c => new
            {
                Id = c.Int(nullable: false, identity: true),
            })
        .PrimaryKey(t => t.Id);
    
    CreateTable(
        "dbo.Dogs",
        c => new
            {
                Id = c.Int(nullable: false, identity: true),
            })
        .PrimaryKey(t => t.Id);
    
    CreateTable(
        "dbo.Pigs",
        c => new
            {
                PigId = c.Guid(nullable: false),
            })
        .PrimaryKey(t => t.PigId);
    

    If for some reason Team must have its own Id; change the model as follow:

    public class Team
    {
    
        public int TeamId { get; set; }
    
        //....
    }
    

    Mapping:

    modelBuilder.Entity<Team>()
        .HasKey(i => i.TeamId);
    
    //if you want to make the teamId an auto-generated column
    modelBuilder.Entity<Team>()
         .Property(i => i.TeamId).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);
    
    //if you want to make the cat, dog, and pig combination unique
    modelBuilder.Entity<Team>()
        .Property(i => i.CatId)
        .HasColumnAnnotation(IndexAnnotation.AnnotationName,
        new IndexAnnotation(
            new IndexAttribute("IX_TeamComp", 1) { IsUnique = true }));
    
    modelBuilder.Entity<Team>()
        .Property(i => i.DogId)
        .HasColumnAnnotation(IndexAnnotation.AnnotationName,
        new IndexAnnotation(
            new IndexAttribute("IX_TeamComp",2) { IsUnique = true }));
    
    modelBuilder.Entity<Team>()
        .Property(i => i.PigId)
        .HasColumnAnnotation(IndexAnnotation.AnnotationName,
        new IndexAnnotation(
            new IndexAttribute("IX_TeamComp", 3) { IsUnique = true }));