Dustin Horne

Developing for fun...

Many to Many Relationships in Entity Framework 7

Note:  The information in this post is way outdated.  For a more in depth started on EF Core and different relationship mappings please refer to my updated Deeper Dive Into EF Core post.

In the coming days I'll be introducing a new series of posts about working with MVC 6 and WebAPI in ASP .NET 5.  This will include demonstrations using Entity Framework 7 as I dive into it.  In the meantime, though, I wanted to demonstrate something that functions differently in EF 7 than in previous iterations: Many-to-Many relationships.

For clarity, I am using Entity Framework 7.0.0-rc1-final.  Diving into it has been an interesting journey as many existing examples leveraging Entity Framework 7 are no longer accurate as they are for versions prior to the release candidate and many things have changed.  Even initial configuration has been a challenge.  When it comes to many-to-many relationships there are a few examples of how to implement them and all of them center on one approach, so I wanted to show you two.

One big change to be aware of is that you now must have a third entity to associate the two sides of your relationship.  It sounds like Microsoft will be adding the ability to add these navigation properties and setup the relationships using shadow entities in the future, but for release 1.0 it will not be in place.  Coming previously from primarily an EDMX Model-First focused background, this bothered me.  It didn't feel clean and I didn't like having to build an extra class that seemed to do nothing other than act as glue and tell EF Migrations how to construct the tables.  However, after looking at it and thinking it over, I now prefer this approach.  It allows us to write code that more closely mimics our database schema.

So how do we setup our relationships?  Well, first let's setup some models that we want to relate.  Let's say we have an online store.  Our store will have Products and Categories.  Each product can belong to multiple categories and each category can contain multiple products.  We will make our example simple... each Category will just have Id and Name properties, as will Product.  We will also add a mapping entity called ProductCategory which has columns for CategoryId and ProductId.  It also has navigation properties for Product and Category.  Both the Product and Category entities will contain an ICollection<ProductCategory>.

Our initial entities look as follows:

    public class Category
    {
        [Key]
        public int Id { get; set; }

        public string Name { get; set; }

        public ICollection<ProductCategory> ProductCategories { get; set; }
    }

    public class Product
    {
        [Key]
        public int Id { get; set; }

        public string Name { get; set; }

        public ICollection<ProductCategory> ProductCategories { get; set; }
    }

    public class ProductCategory
    {
        public int ProductId { get; set; }
        public Product Product { get; set; }

        public int CategoryId { get; set; }
        public Category Category { get; set; }
    }

Now we will configure the ProductCategory entity so migrations will propertly create the mapping tables and Entity Framework can load the related entities.  The first method to configure these relationships (and included in the latest entity framework official documentation) is to use your DbContext class.  In particular, we will override the OnModelCreating method and build our mappings.

The first thing we want to do is define the compound key for ProductCategory:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<ProductCategory>().HasKey(x => new { x.ProductId, x.CategoryId });
}

Next, we need to define the relationships between the mapping table and the other two tables.  We have to do this one at a time.  We use the ModelBuilder to define what entity we are mapping to (HasOne), what it maps to (WithMany) and what the foreign key of that relationship is (HasForeignKey).  We do that for both the Product and the Category, mapping both to the ProductCategory table.  Our OnModelCreating method now looks as follows:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<ProductCategory>().HasKey(x => new { x.ProductId, x.CategoryId });

    modelBuilder.Entity<ProductCategory>()
        .HasOne(pc => pc.Product)
        .WithMany(p => p.ProductCategories)
        .HasForeignKey(pc => pc.ProductId);

    modelBuilder.Entity<ProductCategory>()
        .HasOne(pc => pc.Category)
        .WithMany(c => c.ProductCategories)
        .HasForeignKey(pc => pc.CategoryId);

}

Now we have a relationship defined.  We cannot directly access Categories from Product... instead we have to access Product.ProductCategories[x].Category.  It can be problematic for upgrading older EF6 projects, but it's only one extra step in new deployments and better represents how the data is related in the database.

While that wasn't too difficult, it does require a fair amount of entity wiring to be done inside of your DbContext.  I prefer to localize my definitions as much as possible so I prefer to use Attributes to define my relationships.  On a side note, if you prefer to use pure POCOs with no entity specific attributes, mapping everything within the context of your data layer (DbContext project) is more appropriate.  Fortunately, that's very simple to do.  It does require one magic string for each of the foreign keys though.  I wish Microsoft would implement a generic ForeignKey attribute that lets you map properties using a lambda, but that's a discussion for another day.  For this method, we still need to map the composite key using the Fluent API, so you will need to leave the first line in OnModelCreating that defines the composite key.  Everything else can be removed.

We are going to generate the same mapping by adding attributes to our ProductCategory entity class.  The attributes we will be adding exist in the System.ComponentModel.DataAnnotations and System.ComponentModel.DataAnnotations.Schema namespaces.  We will add ForeignKey attributes to both foreign Id properties and map them to the navigation properties.  Our new ProductCategories entity model now looks like this:

public class ProductCategory
{
    [ForeignKey("Product")]
    public int ProductId { get; set; }
    public Product Product { get; set; }

    [ForeignKey("Category")]
    public int CategoryId { get; set; }
    public Category Category { get; set; }
}

And that's all there is to it.  You now have two ways to map many-to-many relationships in Entity Framework 7.