A many-to-many (N
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>();
}
Configuring the N Relationship
In relational databases, many-to-many (N
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”).
| Feature | Implicit Table | Explicit Table |
|---|---|---|
| Complexity | Low | Medium/High |
| Additional Data | Not supported | Supported |
| Control over Schema | Limited | Complete |
| Performance | Good | Optimal |
| Recommended for | Simple N | Relationships with metadata |
Entity Framework Core does not support direct Data Annotations for N
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"));
}
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)
);
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; }
}
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);
}
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")
);
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);
Practical Examples
Basic query with implicit join table
// Get all courses for a student
var student = context.Students
.Include(e => e.Courses)
.FirstOrDefault(e => e.StudentId == 1);
// Get all students in a course
var course = context.Courses
.Include(c => c.Students)
.FirstOrDefault(c => c.CourseId == 101);
Query with explicit join table
var history = context.Enrollments
.Where(i => i.StudentId == 1)
.Include(i => i.Course)
.OrderBy(i => i.EnrollmentDate)
.ToList();
Insert N relationship with implicit table
// Get existing student and course
var student = context.Students.Find(1);
var course = context.Courses.Find(101);
// Establish relationship
student.Courses.Add(course);
context.SaveChanges();
Insert with explicit table (and additional data)
var newEnrollment = new Enrollment
{
StudentId = 1,
CourseId = 101,
EnrollmentDate = DateTime.Now,
Grade = null
};
context.Enrollments.Add(newEnrollment);
context.SaveChanges();
