Modeling Many-to-Many Relationships
A many-to-many relationship - many Students enrolled in many Courses - is modeled with a collection navigation property on each side (ICollection<Course> Courses on Student, ICollection<Student> Students on Course) and nothing else. Since EF Core 5.0, this pattern of two skip navigations is enough for EF Core to automatically create and manage a hidden join table behind the scenes, without you needing to define a join entity class.
Cricket analogy: Many-to-many is like players belonging to multiple IPL franchises across different seasons while each franchise fields multiple players - a Student-Courses relationship works the same way, with EF Core managing the many-to-many link like a transfer ledger behind the scenes.
Implicit Join Table vs Explicit Join Entity
By default, EF Core creates an implicit join entity type (e.g., CourseStudent) with just two FK columns, invisible in your C# code but visible as a table in migrations. When you need more than just the two foreign keys - say, an EnrollmentDate or Grade on the join itself - you introduce an explicit join entity class (Enrollment) and model the relationship as two one-to-many relationships instead, giving you full control over the extra columns.
Cricket analogy: An implicit join table is like an informal tally of which players faced which bowlers, invisible in the scorebook's main pages; an explicit join entity is like a dedicated ball-by-ball log that also records the outcome of each specific delivery, similar to adding a Grade column.
Adding Payload Data to the Join Entity
To add payload data via an explicit join entity, you configure it with modelBuilder.Entity<Student>().HasMany(s => s.Courses).WithMany(c => c.Students).UsingEntity<Enrollment>(j => j.HasOne(e => e.Course).WithMany(), j => j.HasOne(e => e.Student).WithMany()). Once configured this way, Enrollment becomes a first-class entity with its own DbSet<Enrollment>, so you can query enrollment records directly, filter by Grade, or add validation logic that a hidden implicit join table could never support.
Cricket analogy: Configuring UsingEntity<Enrollment> is like a cricket board formally registering a player-contract document (not just a name on a squad list) so admins can query contract details like match fee directly, rather than only seeing 'player X plays for team Y'.
public class Student
{
public int Id { get; set; }
public string Name { get; set; } = null!;
public ICollection<Course> Courses { get; set; } = new List<Course>();
}
public class Course
{
public int Id { get; set; }
public string Title { get; set; } = null!;
public ICollection<Student> Students { get; set; } = new List<Student>();
}
// Explicit join entity with payload data
public class Enrollment
{
public int StudentId { get; set; }
public Student Student { get; set; } = null!;
public int CourseId { get; set; }
public Course Course { get; set; } = null!;
public DateTime EnrollmentDate { get; set; }
public string? Grade { get; set; }
}
modelBuilder.Entity<Student>()
.HasMany(s => s.Courses)
.WithMany(c => c.Students)
.UsingEntity<Enrollment>(
j => j.HasOne(e => e.Course).WithMany(),
j => j.HasOne(e => e.Student).WithMany());Since EF Core 5.0, plain skip navigations (ICollection on both sides, no explicit join class) are enough to get a working many-to-many relationship - you only need to introduce an explicit join entity like Enrollment once you need extra columns on the relationship itself.
Skip Navigations Under the Hood
Both the implicit and explicit forms rely on EF Core's 'skip navigation' concept - a navigation property that skips over the join entity to expose the related entity directly (student.Courses instead of student.Enrollments.Select(e => e.Course)). You can still query through the join entity explicitly when you need the payload data, or use the skip navigation when you just need the related entities themselves, and EF Core keeps both views consistent automatically.
Cricket analogy: A skip navigation is like a fan checking a player's 'teams played for' list directly instead of manually cross-referencing every individual contract record - EF Core exposes student.Courses the same convenient way, skipping past the Enrollment join entity.
Once you introduce an explicit join entity, you must also add its composite key configuration (HasKey(e => new { e.StudentId, e.CourseId })) if you want to prevent duplicate enrollments - EF Core won't infer a composite key for a join entity automatically the way it does for the implicit form.
- Many-to-many is modeled with a collection navigation (skip navigation) on both participating entities.
- EF Core 5+ auto-creates a hidden implicit join table when no explicit join entity is declared.
- Use an explicit join entity when you need payload data like EnrollmentDate or Grade.
- UsingEntity<TJoinEntity>() configures the explicit join entity's two one-to-many relationships.
- An explicit join entity gets its own DbSet, so it can be queried directly.
- A composite key on the join entity must be configured explicitly to prevent duplicates.
Practice what you learned
1. What C# language feature lets you model a many-to-many relationship without an explicit join entity in EF Core 5+?
2. When must you introduce an explicit join entity for a many-to-many relationship?
3. Which Fluent API method configures an explicit join entity for a many-to-many relationship?
4. What must you manually configure on an explicit join entity that EF Core infers automatically for the implicit form?
Was this page helpful?
You May Also Like
One-to-Many Relationships
Learn how EF Core models one-to-many relationships through navigation properties and foreign keys, including required vs optional relationships and delete behavior.
Entity Classes and Conventions
Learn how EF Core discovers and maps plain C# classes to database tables using convention-based modeling, including primary keys, shadow properties, and backing fields.
Data Annotations vs Fluent API
Compare EF Core's two configuration approaches - inline Data Annotations and the more powerful Fluent API - and learn when each is the right tool.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics