Fluent API, many-to-many in Entity Framework Core
EF Core 5.0 RC1+
As of EF Core 5.0 RC1, it's possible to do this without an explicit join table. EF Core is able to configure a mapping for the many-to-many relationship shown in your question without requiring you to create a PersonClub
type.
See What's New in EF Core 5.0, RC1, Many-to-many in the official docs for more information.
Previous Versions
This is not yet possible in EF Core without using an explicit class for the join. See here for an example of how to do that.
There's an open issue on Github asking for the ability to do this without the need for an explicit class, but it has not yet been completed.
Using your scenario, the example I linked would recommend the following entity classes:
public class Person
{
public int PersonId { get; set; }
public virtual ICollection<PersonClub> PersonClubs { get; set; }
}
public class Club
{
public int ClubId { get; set; }
public virtual ICollection<PersonClub> PersonClubs { get; set; }
}
public class PersonClub
{
public int PersonId { get; set; }
public Person Person { get; set; }
public int ClubId { get; set; }
public Club Club { get; set; }
}
The following OnModelCreating
would then be used for setup:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PersonClub>()
.HasKey(pc => new { pc.PersonId, pc.ClubId });
modelBuilder.Entity<PersonClub>()
.HasOne(pc => pc.Person)
.WithMany(p => p.PersonClubs)
.HasForeignKey(pc => pc.PersonId);
modelBuilder.Entity<PersonClub>()
.HasOne(pc => pc.Club)
.WithMany(c => c.PersonClubs)
.HasForeignKey(pc => pc.ClubId);
}
Be sure to go to the open issue I linked and voice your frustration if you feel the need.
EDIT: The open issue suggests using a simple Select
to navigate through this somewhat cumbersome hierarchy. In order to get from a PersonId
to a collection of Club
s, you can use SelectMany
. e.g.:
var clubs = dbContext.People
.Where(p => p.PersonId == id)
.SelectMany(p => p.PersonClubs);
.Select(pc => pc.Club);
I can't vouch for whether this is truly a "best practice", but it should certainly do the trick and I think its fair to say it's not overly ugly.
So every Person
has zero or more Clubs
and every Club
has zero or more Persons
. As you stated correctly, this is a proper many-to-many relation.
You probably know that a relational database needs an extra table to implement this many-to-many relationship. The nice thing about entity framework, is that it recognizes this relationship and creates this extra table for you.
At first glance it seems a problem that this extra table is not a dbSet
in your DbContext
: "How to perform a join with this extra table if I don't have a DbSet
for it?".
Luckily, you don't need to mention this extra table in your queries.
If you need a query like "Give me all 'Clubs' that ... from every 'Person' who ..." don't think in joins. Instead use the ICollections!
Get all "John Doe" persons with all Country clubs they attend:
var result = myDbContext.Persons
.Where(person => person.Name == "John Doe")
.Select(person => new
{
PersonId = person.Id,
PersonName = person.Name,
AttendedCountryClubs = person.Clubs
.Where(club => club.Type = ClubType.CountryClub),
};
Entity framework will recognize that a join with the extra many-to-many table is needed, and will perform this join, without you mentioning this extra table.
The other way round: Get all country clubs with their "John Doe" Persons:
var result = myDbContext.Clubs
.Where(club => club.Type = ClubType.CountryClub)
.Select(club => new
{
ClubId = club.Id,
ClubName = club.Name,
AnonymousMembers = club.Persons
.Where(person => person.Name == "John Doe"),
}
I've experienced that once I started to think in the resulting collections that I want instead of the joins I needed to get these collections I found that I hardly use the joins. This is the case for one-to-many relations as well as many-to-many relations. Entity framework will internally use the proper joins.
The correct "setup" for this is:
public class Person
{
public int PersonId { get; set; }
public virtual ICollection<PersonClub> PersonClubs { get; set; }
}
public class Club
{
public int ClubId { get; set; }
public virtual ICollection<PersonClub> PersonClubs { get; set; }
}
public class PersonClub
{
public int PersonId { get; set; }
public Person Person { get; set; }
public int ClubId { get; set; }
public Club Club { get; set; }
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PersonClub>()
.HasKey(pc => new { pc.PersonId, pc.ClubId });
}
So, this block for configuring the "glue-table" is not necessary as in @Kirk example:
modelBuilder.Entity<PersonClub>()
.HasOne(pc => pc.Person)
.WithMany(p => p.PersonClubs)
.HasForeignKey(pc => pc.PersonId);
modelBuilder.Entity<PersonClub>()
.HasOne(pc => pc.Club)
.WithMany(c => c.PersonClubs)
.HasForeignKey(pc => pc.ClubId);