Polymorphic Associations Using EF Core

Author by Dalton Smith

For this example, I will be modeling a website that has pictures, videos, and blogs that can all contain comments. There will also be certain pages/posts that cannot contain comments, and these pages inherit from a shared interface as the pictures, videos, and blogs. We want to use one table, "comments," to store comments for each of these post types. One's first thought might be to create a structure like so: Initial_Design-(1).PNG

 

This obviously cannot work, as the PageID foreign key constraint will only fulfill one of the three FK constraints on the table.

 

Another thought might be to create a composite foreign key on the comments table, composed of the PageID and CommentType. This could work, but I found there to be a more standard approach to solving this problem using the Table per Class or Table per Type pattern. An updated diagram to reflect this pattern would look similar to this: final_Design.PNGIn this diagram, CommentPage represents a base type for pages that can contain comments and will have its own table in the database. As you can see, the database here doesn't have any knowledge of a hierarchy between CommentPage and the pages that can contain comments. This is where some of the EF Core "magic" will come into play to wire up a working system. 

To start, Blog, Picture, and Video will not actually inherit from the CommentPage class. This is because, by default, EF Core would configure this using the table per hierarchy pattern, putting all entries for blogs, pictures, or videos into the same table. Instead, each of these classes will contain a CommentPage navigation property:

public virtual CommentPage CommentPage { get; set; }

The CommentPage class will contain a navigation property that stores all of the Comments for that specific page, so rather than having all the comments inside the Blog, Picture, or Video class, they're abstracted out one layer. 

This is looking good so far - each comment-able page has the CommentPage navigation property, which in turn contains the comments. The CommentPage property can't, however, just be easily .Include()'d, as no foreign keys can be configured to the PageID. This brings us to the final step: populating the CommentPage navigation property.

This example shows querying a video by ID and including the CommentPage property.

public Video GetById(Guid id) {

  var commentPage = context

    .CommentPages

    .Single(c => c.PageId == id);

  

  context.Entry(commentPage).Collection(p => p.Comments).Load();

 

  var video = context.Videos.FirstOrDefault(v => v.Id == id);

  video.CommentPage = commentPage;

  return video;

}

 

This concept can be applied across repositories to include the CommentPage and Comments whenever needed.

Overall, Polymorphic Associations are a good way to reduce the number of tables and entities needed to represent essentially the same object, and can be easy to understand and use when implemented in the way shown in this post. 

Tags in this Article