The data model you are showing is fine, but one thing is clear. You cannot match this as a pure many-to-many association. This is only possible if the BookContributors connection BookContributors contains only BookId and ContributorId .
Therefore, you always need an explicit BookContributor class, and getting a collection of one of the types of contributors will always have this basic form:
book.BookContributors .Where(bc => bc.Type == type) .Select(bc => bc.Contributor)
Clumsy compared to what you mean. I'm afraid I can't get around this. There are a few options left in the implementation details.
Option 1: Get all participants, filter later.
First let me get the base model correctly:
public class Book { public int BookId { get; set; } public string Title { get; set; } public virtual ICollection<BookContributor> BookContributors { get; set; } } public class Contributor { public int ContributorId { get; set; } public string Name { get; set; } public virtual ICollection<BookContributor> BookContributors { get; set; } } public class BookContributor { public int BookId { get; set; } public virtual Book Book { get; set; } public int ContributorId { get; set; } public virtual Contributor Contributor { get; set; } public string Type { get; set; } }
And display:
protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<Book>().HasMany(b => b.BookContributors) .WithRequired(bc => bc.Book) .HasForeignKey(bc => bc.BookId); modelBuilder.Entity<Contributor>().HasMany(c => c.BookContributors) .WithRequired(bc => bc.Contributor) .HasForeignKey(bc => bc.ContributorId); modelBuilder.Entity<BookContributor>() .HasKey(bc => new {bc.BookId, bc.ContributorId, bc.Type}); }
(by the way, here I avoid the term “Discriminator” because it involves the inheritance of TPH, which is not yet applicable).
Now you can add properties to the Book as follows:
[NotMapped] public IEnumerable<Contributor> Writers { get { return BookContributors.Where(bc => bc.Type == "writer") .Select(bc => bc.Contributor); } }
The disadvantage of this approach is that you should always ensure that books are downloaded from their BookContributors and their Contributor , or this lazy download. And you cannot use these properties directly in a LINQ query. In addition, it is somewhat difficult to get books and only their unique contributors (i.e., Different).
Option 2: Inheritance is essentially the same
You can make BookContributor abstract base class with a number of descendants:
public abstract class BookContributor { public int Id { get; set; } public int BookId { get; set; } public virtual Book Book { get; set; } public int ContributorId { get; set; } public virtual Contributor Contributor { get; set; } } public class Artist : BookContributor { } public class Writer : BookContributor { }
BookContributor now needs the surrogate key Id , because EF will now use the Discriminator field, which is hidden, so it cannot be configured as part of the primary key.
Book can now have properties such as ...
[NotMapped] public ICollection<Artist> Artists { get { return BookContributors.OfType<Artist>().ToList(); } }
... but they will still have the same disadvantages as those mentioned above. The only possible advantage is that now you can use types (with compile-time checking) instead of strings (or enumeration values) to move on to different types of BookContributor .
option 3: another model
Perhaps the most promising approach is a slightly different model: books and contributors, where each association between them can have a collection of contributor types. BookContributor now looks like this:
public class BookContributor { public int BookId { get; set; } public virtual Book Book { get; set; } public int ContributorId { get; set; } public virtual Contributor Contributor { get; set; } public virtual ICollection<BookContributorType> BookContributorTypes { get; set; } }
And a new type:
public class BookContributorType { public int ID { get; set; } public int BookId { get; set; } public int ContributorId { get; set; } public string Type { get; set; } }
Modified Display:
modelBuilder.Entity<BookContributor>().HasKey(bc => new { bc.BookId, bc.ContributorId });
Additional display:
modelBuilder.Entity<BookContributor>().HasMany(bc => bc.BookContributorTypes).WithRequired(); modelBuilder.Entity<BookContributorType>().HasKey(bct => bct.ID);
With this model, you can simply get books and their individual contributors if you are not interested in the types of participants ...
context.Books.Include(b => b.BookContributors .Select(bc => bc.Contributor))
... or with types ...
context.Books.Include(b => b.BookContributors .Select(bc => bc.Contributor)) .Include(b => b.BookContributors .Select(bc => bc.BookContributorTypes));
... or books only with writers ...
context.Books.Select(b => new { Book = b, Writers = b.BookContributors .Where(bc => bc.BookContributorTypes .Any(bct => bct.Type == "artist")) })
Again, the last request can be wrapped in a property ...
[NotMapped] public ICollection<Artist> Artists { get { return BookContributors .Where(bc => bc.BookContributorTypes .Any(bct => bct.Type == "artist")) .Select(bc => bc.Contributor).ToList(); } }
... however, with all the above warnings.