entity-framework-relaciones-muchos-a-muchos

Many to Many Relationships in Entity Framework

  • 5 min

A many-to-many (N) relationship occurs when multiple records in one table can be associated with multiple records in another table.

This type of relationship is more complex due to the need for an intermediate table to store the associations (called a bridge table or join table).

Let’s see it by modeling a course with students (sorry for the cliché), where,

public class Student
{
    public int StudentId { get; set; }
    public string Name { get; set; }
    
    // Navigation property
    public ICollection<Course> Courses { get; set; } = new List<Course>();
}

public class Course
{
    public int CourseId { get; set; }
    public string CourseName { get; set; }
    
    // Navigation property
    public ICollection<Student> Students { get; set; } = new List<Student>();
}
Copied!

Configuring the NRelationship

In relational databases, many-to-many (N) relationships cannot exist directly between two tables.

To implement them, an intermediate table is required (called a bridge table or join table) that stores the valid combinations between both entities.

We can let Entity Framework create this table automatically for us, or create the corresponding Entity ourselves and handle the associations manually.

  • Use an implicit table if you only need to relate records (e.g., “Student X is in Course Y”).
  • Use an explicit table if you need to save information about the relationship itself (e.g., “when they enrolled” or “what grade they have”).
FeatureImplicit TableExplicit Table
ComplexityLowMedium/High
Additional DataNot supportedSupported
Control over SchemaLimitedComplete
PerformanceGoodOptimal
Recommended forSimple NrelationshipsRelationships with metadata

Entity Framework Core does not support direct Data Annotations for Nrelationships. Fluent API is required for complete configuration.

Implicit Join Table (Automatic)

Entity Framework Core (since version 5) can automatically generate this table without you having to create a class for it.

Configuration with Fluent API:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Student>()
        .HasMany(e => e.Courses)
        .WithMany(c => c.Students)
        .UsingEntity(j => j.ToTable("StudentCourses"));
}
Copied!

This would be the generated structure in the DB

CREATE TABLE StudentCourses (
    StudentsStudentId INT NOT NULL,
    CoursesCourseId INT NOT NULL,
    PRIMARY KEY (StudentsStudentId, CoursesCourseId),
    FOREIGN KEY (StudentsStudentId) REFERENCES Students(StudentId),
    FOREIGN KEY (CoursesCourseId) REFERENCES Courses(CourseId)
);
Copied!

Explicit Join Table (Manual)

When you need to save additional information in the relationship (like metadata), you must create an explicit entity for the join table.

In the previous example, this explicit table could be Inscripcion,

public class Enrollment
{
    public int StudentId { get; set; }
    public int CourseId { get; set; }
    public DateTime EnrollmentDate { get; set; } // Additional data
    public decimal? Grade { get; set; }         // Additional data
    
    // Navigation properties
    public Student Student { get; set; }
    public Course Course { get; set; }
}
Copied!

We would configure it in Fluent API like this:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Enrollment>()
        .HasKey(i => new { i.StudentId, i.CourseId }); // Composite key
        
    modelBuilder.Entity<Enrollment>()
        .HasOne(i => i.Student)
        .WithMany(e => e.Enrollments)
        .HasForeignKey(i => i.StudentId);
        
    modelBuilder.Entity<Enrollment>()
        .HasOne(i => i.Course)
        .WithMany(c => c.Enrollments)
        .HasForeignKey(i => i.CourseId);
}
Copied!

When to prefer the manual table?

  • Greater control and provides its own entity: For example, if we needed to add fields like FechaInscripcion, Calificacion, etc.
  • But requires manual configuration: Requires defining foreign keys and relationships in Fluent API.

In this example, the name Inscripcion worked very well for us because it aligns perfectly with the concept.

But if it didn’t correspond to a concept, it would also have been common to simply call it EstudianteCursos.

Advanced Configurations

Custom options to adjust the behavior of the data model in Entity Framework Core.

Naming columns in the implicit join table

Defines the column names in an automatically generated join table for a many-to-many relationship.

modelBuilder.Entity<Student>()
    .HasMany(e => e.Courses)
    .WithMany(c => c.Students)
    .UsingEntity<Dictionary<string, object>>(
        "StudentCourses",
        j => j.HasOne<Course>().WithMany().HasForeignKey("CourseId"),
        j => j.HasOne<Student>().WithMany().HasForeignKey("StudentId")
    );
Copied!

Configure cascade delete

Sets that when a student is deleted, their associated enrollments are also automatically deleted.

modelBuilder.Entity<Enrollment>()
    .HasOne(i => i.Student)
    .WithMany(e => e.Enrollments)
    .HasForeignKey(i => i.StudentId)
    .OnDelete(DeleteBehavior.Cascade);
Copied!

Practical Examples