Thursday, December 3, 2020

EF Core 5.0: Many to Many Relationships

One of the nicest features available in EF Core 5.0 is now you can define a many-to-many relationship without explicitly mapping the join table. 

With earlier EF Core versions, you didn't have the luxury of doing something like this,

Example 1:

public class Student
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<Course> Courses { getset; }
}

public class Course
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<Student> Students { getset; }
}

You needed to explicitly create the join table, something like below.

Example 2:

public class Student
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<CourseStudent> CourseStudents { getset; }
}

public class Course
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<CourseStudent> CourseStudents { getset; }
}

public class CourseStudent
{
    public int Id { getset; }

    public Course Course { getset; }     public Student Student { getset; }
}

But with EF Core 5.0, you don't have to do that anymore, the code in Example 1 would work just fine. EF will understand we need a join table and it will create one automatically.

Database
You can use it like below.
using var context = new MyDbContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

var john = new Student { Name = "John" };
var jane = new Student { Name = "Jane" };
var tim = new Student { Name = "Tim" };

var csharp = new Course { Name = "C#", Students = new List<Student> { john, jane } };
var sql = new Course { Name = "SQL", Students = new List<Student> { tim } };

context.AddRange(john, jane, tim, csharp, sql);
await context.SaveChangesAsync();

List<Student> csharpStudents = await context.Students .Where(x => x.Courses.Any(sc => sc.Name == csharp.Name)) .ToListAsync();

foreach (Student student in csharpStudents)
{
    Console.WriteLine(student.Name);
}

Output,

John
Jane

Often times, you need additional data (known as payload properties) in the join table. In our case here, maybe the date that the student got enrolled with the Course. So I can design the data model like below. 

public class Student
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<Course> Courses { getset; }

    public ICollection<CourseStudent> CourseStudents { getset; }
}

public class Course
{
    public int Id { getset; }

    public string Name { getset; }

    public ICollection<Student> Students { getset; }

    public ICollection<CourseStudent> CourseStudents { getset; }
}

public class CourseStudent
{
    public Course Course { getset; }

    public Student Student { getset; }

    public DateTime EnrolledDate { getset; }
}

If you have noticed in the above code, you can even specify a navigation property to the Join table. Let's see how it can make our life easy in certain cases.

And EF Core allows full customization of the join table, something like below.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Student>()
        .HasMany(x => x.Courses)
        .WithMany(x => x.Students)
        .UsingEntity<CourseStudent>(
            x => x
                .HasOne(x => x.Course)
                .WithMany(x => x.CourseStudents),
            x => x
                .HasOne(x => x.Student)
                .WithMany(x => x.CourseStudents),
            x =>
            {
                x.Property(x => x.EnrolledDate).HasDefaultValueSql("CURRENT_TIMESTAMP");
            });
}

Then I can do things like this.

using var context = new MyDbContext();
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();

var john = new Student { Name = "John" };
var jane = new Student { Name = "Jane" };
var tim = new Student { Name = "Tim" };

var csharp = new Course
{
Name = "C#",
    CourseStudents = new List<CourseStudent>
    {
        new CourseStudent
        {
            Student = john,
            EnrolledDate = new DateTime(2020, 06, 01)
        },
        new CourseStudent
        {
            Student = jane,
            EnrolledDate = new DateTime(2020, 10, 01)
        }
    }
};
var sql = new Course { Name = "SQL", Students = new List<Student> { tim } };

context.AddRange(john, jane, tim, csharp, sql);
await context.SaveChangesAsync();

List<Course> courses = await context.Courses
    .ToListAsync();

foreach (Course course in courses)
{
    Console.WriteLine($"{course.Name}");

    // If you don't want payload property information, you can just use Students 
    foreach (Student student in course.Students)
    {
     Console.WriteLine($"-- {student.Name}");
    }

    // If you want payload property information, you can use CourseStudents, the navigation to join table  
    foreach (CourseStudent courseStudent in course.CourseStudents)
    {
        Console.WriteLine($"-- {courseStudent.Student.Name} : {courseStudent.EnrolledDate}");
    }
}

Output,

C#
// Without using Join Table
-- John
-- Jane

// With using Join Table
-- John : 6/1/2020 12:00:00 AM
-- Jane : 10/1/2020 12:00:00 AM

SQL
// Without using Join Table
-- Tim

// With using Join Table
-- Tim : 12/3/2020 9:17:00 PM

Hope this helps.

To know more on EF Core 5, you can read: What's New in EF Core 5.0

Happy Coding.

Regards,
Jaliya

No comments:

Post a Comment