Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Explicit Loading N:M with Filtering [duplicate]

I am trying to filter the results from an explicit load in EntityFramework.

The explicit loading works when I do not apply any filter but it does not load results once the filter is applied.

Classes

public partial class Student
{
    public int StudentId { get; set; }
    public int CourseId { get; set; }
    public string Name { get; set; }
    public string Status { get; set; }
    public virtual ICollection<Grade> Grades { get; set; }
}

public partial class Grade
{
    public int GradeId { get; set; }
    public string Value { get; set; }
    public string Status { get; set; }
    public virtual ICollection<Student> Students { get; set; }
}

Fluent API Mapping

modelBuilder.Entity<Grade>()
    .HasMany(e => e.Students)
    .WithMany(x => x.Grades)
    .Map(m => m.ToTable("StudentGrades").MapLeftKey("GradeId").MapRightKey("StudentId"));

Usage

This works and populates the student.Grades property.

using (var context = new Model1())
{
    context.Configuration.LazyLoadingEnabled = false;

    var student = context.Students.Single(x => x.StudentId == 1);
    context.Entry(student).Collection(x => x.Grades).Load();
}

The SQL that is generated looks like this:

SELECT 
[Extent2].[GradeId] AS [GradeId], 
[Extent2].[Value] AS [Value], 
[Extent2].[Status] AS [Status]
FROM  [dbo].[StudentGrades] AS [Extent1]
INNER JOIN [dbo].[Grades] AS [Extent2] ON [Extent1].[GradeId] = [Extent2].[GradeId]
WHERE [Extent1].[StudentId] = 1 // this is parameterized in the actual hit.

When I run this query I get the full results.

However, when I apply filtering and use the following line, it does not populate student.Grades.

context.Entry(student).Collection(x => x.Grades).Query().Where(x => x.Status == "A").Load();

This line generates this query:

SELECT 
[Extent2].[GradeId] AS [GradeId], 
[Extent2].[Value] AS [Value], 
[Extent2].[Status] AS [Status]
FROM  [dbo].[StudentGrades] AS [Extent1]
INNER JOIN [dbo].[Grades] AS [Extent2] ON [Extent1].[GradeId] = [Extent2].[GradeId]
WHERE ([Extent1].[StudentId] = 1) AND ('A' = [Extent2].[Status])
//the "1" is parameterized in the actual hit.

When I run this manually against the DB I get the correctly filtered results within SQL Server. The problem is that this doesn't populate student.Grades in the C# object.

like image 357
Matt Rowland Avatar asked Jul 21 '16 20:07

Matt Rowland


People also ask

How many types of loading are there in Entity Framework?

Entity Framework supports three ways to load related data - eager loading, lazy loading and explicit loading.

How do I turn off lazy loading in Entity Framework?

To turn off lazy loading for all entities in the context, set its configuration property to false.

Why do we use lazy loading in Entity Framework?

Lazy loading is the process whereby an entity or collection of entities is automatically loaded from the database the first time that a property referring to the entity/entities is accessed. Lazy loading means delaying the loading of related data, until you specifically request for it.

How do you load entities or data in EF?

Entity Framework supports the following three methods to load related data. In Eager Loading, all relevant data for an entity is loaded at the time of the query of the entity in the context object. Eager Loading can be done by using the "Include" method. To perform Eager Loading, Lazy Loading must be disabled.


1 Answers

This technique is mentioned in the MSDN Article - Applying filters when explicitly loading related entities section, so it's supposed to be supported and working. Strangely enough, it's working for one-to-many relationship, many-to-many with explicit link table and 2 one-to-many associations, but not for many-to-many with implicit link table.

I have no explanation why is that (didn't find related documentation). I also have no explanation why, but combining it with a request for eagerly loading the other collection does the trick:

context.Entry(student).Collection(s => s.Grades)
    .Query().Where(g => g.Status == "A")
    .Include(g => g.Students)
    .Load();

The drawback of this (as mentioned in the comments) is that it would also load a lot of students that belong to the loaded grades.

So the better way would be to use the explicit link table and relationships like this:

Model:

public partial class Student
{
    public int StudentId { get; set; }
    public int CourseId { get; set; }
    public string Name { get; set; }
    public string Status { get; set; }
    public virtual ICollection<StudentGrade> StudentGrades { get; set; }
}

public partial class Grade
{
    public int GradeId { get; set; }
    public string Value { get; set; }
    public string Status { get; set; }
    public virtual ICollection<StudentGrade> StudentGrades { get; set; }
}

public class StudentGrade
{
    public int StudentId { get; set; }
    public int GradeId { get; set; }
    public virtual Student Student { get; set; }
    public virtual Grade Grade { get; set; }
}

Configuration:

modelBuilder.Entity<StudentGrade>()
   .ToTable("StudentGrades")
   .HasKey(e => new { e.GradeId, e.StudentId });

modelBuilder.Entity<StudentGrade>()
    .HasRequired(e => e.Grade)
    .WithMany(x => x.StudentGrades)
    .HasForeignKey(e => e.GradeId)
    .WillCascadeOnDelete();

modelBuilder.Entity<StudentGrade>()
    .HasRequired(e => e.Student)
    .WithMany(x => x.StudentGrades)
    .HasForeignKey(e => e.StudentId)
    .WillCascadeOnDelete();

Now the explicit loading does not require tricks and will load the filtered related StudentGrade entities with only GradeId and StudentId fields populated, thus avoiding loading the additional Grade and Student objects:

context.Entry(student).Collection(s => s.StudentGrades)
    .Query().Where(sg => sg.Grade.Status == "A")
    .Load();
like image 131
Ivan Stoev Avatar answered Oct 30 '22 08:10

Ivan Stoev