46

I've searched stackoverflow for a proper solution on generating a many-to-many relationship, using EF Core, Code first and Fluent API.

A simple scenario would be:

public class Person
{
    public Person() {
        Clubs = new HashSet<Club>();
    }
    public int PersonId { get; set; }
    public virtual ICollection<Club> Clubs { get; set; }
}

public class Club
{
    public Club() {
        Persons = new HashSet<Person>();
    }
    public int ClubId { get; set; }
    public virtual ICollection<Person> Persons { get; set; }
}

Please correct me if im wrong but I could honestly not find a question that contains an elaborate explanation on how to do this using the described tools. Can anyone explain how this is done?

Anonymous
  • 1,303
  • 4
  • 19
  • 31

3 Answers3

59

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 Clubs, 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.

Kirk Larkin
  • 84,915
  • 16
  • 214
  • 203
  • Migrating to 5.0 was easy and worked perfectly <3 – Michael Holley Dec 02 '20 at 14:47
  • Can you provide ClubsController methods as an example? I need something like [EnableQuery] public IActionResult Get() { return Ok(_dbContext.Clubs .Include(x => x.PersonClubs) .ThenInclude(x => x.Persons)); } but this is not working – Laser42 Mar 16 '21 at 10:54
  • [OfftopicAttribute] /* This is one of the best answers I had in my life, especially because I'm on a .Net Training and got a deadline for the course project today, without a working database, it just probably saved me*/ – Ivan Silkin Nov 01 '21 at 04:13
25

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);
paul van bladel
  • 1,663
  • 14
  • 18
  • 4
    If you use the [ForeignKey] attribute in the link entity you don’t need any code. Even so: pathetic that this doesn’t work like the earlier version. In 4 years of dev for Core they haven’t been able to make to match the previous version. – Rick Strahl Feb 04 '18 at 04:19
  • 7
    *Not necessary* doesn't mean *incorrect*. Being explicit in configuration doesn't hurt, and actually many times helps avoiding surprises caused by implicit EF Core conventions. There are request for providing a way to get rid of all conventions and require explicit configuration for everything. – Ivan Stoev Jul 05 '18 at 17:40
  • 1
    What about the fluent mappings for `Person` and `Club`? – Cocowalla Feb 27 '19 at 13:26
  • How would one correctly retrieve all Clubs based on a Person and information inside of PersonClub. Like Person.Clubs? – tblev May 04 '21 at 14:40
3

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.

Harald Coppoolse
  • 28,834
  • 7
  • 67
  • 116