Search code examples
c#entity-framework-corepredicatebuilderlinqkit

Guidance on adding filter expression to query


The application I am working on produces reports. I am tryingto implement some of the filtering which can involve up to several dozen fields in various combinations. I had previously used Dapper with string concatenation to build out the SQL but am trying to move to EF 7. It was not to difficult to implement some of the simpler filters using LinqKit with the PredicateBuilder. However, I am a little stuck with this one. The concept is to filter by a field in a child table while having the children tables linked by a non-key field pulling the related information. The below code is a rough representation.

public partial class Person
{
    public int Id {get; set;}
    public string AltId {get; set;}
    public string Name {get; set;}
    public virtual Student Student { get; set; }
    public virtual Employee Employee { get; set; }
}
public partial class Employee
{
    public int Id {get; set;}
    public int PersonId {get; set;}
}
public partial class Student
{
    public int Id {get; set;}
    public int PersonId {get; set;}                 // FK to Person
    public virtual ICollection<StudentTestAvailability> StudentTestAvailability { get; } = new List<StudentTestAvailability>();
    public virtual ICollection<StudentTestHistory> StudentTestHistory { get; } = new List<StudentTestHistory>();
}
public partial class StudentTestAvailability
{
    public int Id {get; set;}
    public int StudentId {get; set;}                // FK to Student
    public DateTime DateAvailable {get; set;}
    public int LocationId {get; set;}               // FK to Locations
    public int TestNameId {get; set;}               // FK to Test names
    public virtual TestNames TestNames { get; set; }
    public virtual TestLocations TestLocations { get; set; }
}
public partial class StudentTestHistory
{
    public int Id {get; set;}
    public int StudentId {get; set;}                // FK to Student
    public DateTime DateTaken {get; set;}
    public int LocationId {get; set;}               // FK to Locations
    public int TestNameId {get; set;}               // FK to Test names
    public virtual TestNames TestNames { get; set; }
    public virtual TestLocations TestLocations { get; set; }
}
public class ReportRepo
{
    public async Task<List<Person>> BuildReport(Filter filter)
    {
        var predicate = PredicateBuilder.New<Person>(true);
        if (string.IsNullOrWhiteSpace(filter.AltId) == false) { predicate = predicate.And(a => a.AltId == filter.AltId); }
        // Several more conditions
        // Insert filter for name/location of test with availability test date = test taken date
        using var context = _factory.CreateDbContext();
        {
            return await context.Person
                .Include(i => i.Employee)
                .Include(i => i.Student).ThenInclude(t => t.StudentTestAvailability)
                .Include(i => i.Student).ThenInclude(t => t.StudentTestTaken)
                .AsExpandable()
                .Where(predicate)
                .ToListAsync();
        }   
    }
}

The filter that is desired is to link the availability and taken tables by the date field retrieving only records that match the nameId and / or locationId, depending on if provided (>0). The SQL previously used was similar to this but more extensive.

SELECT p.*,s.*,sth.*,sta FROM People p
INNER JOIN Student S ON p.id = s.PersonId
LEFT JOIN StudentTestHistory sth ON s.id = sth.StudentId
LEFT JOIN StudentTestAvailability sta ON t.id = sta.StudentId
Where sth.StudentId = tta.StudentId 
    AND sth.DateTaken = sta.DateAvailable 
    AND sth.TestNameId = ata.TestNameId 
    AND sth.locationId = ata.locationId 
    AND (@nameId<1 OR sta.TestNameId=@nameId)
    AND (@locationId<1 OR sta.locationId=@locationId)

Any assistance is appreciated.

EDIT: In response to the question regarding navigation properties. Below are the navigation properties in question.


Solution

  • I have rewritten your query to meet desired SQL. Removed AsExpandable(), it is not needed for PredicateBuilder, and actually I do not see why it is introduced here.

    public class ReportRepo
    {
        public async Task<List<Person>> BuildReport(Filter filter)
        {
            var predicate = PredicateBuilder.New<Person>(true);
            if (string.IsNullOrWhiteSpace(filter.AltId) == false) 
               { predicate = predicate.And(a => a.AltId == filter.AltId); }
    
            // Several more conditions
            // Insert filter for name/location of test with availability test date = test taken date
    
            using var context = _factory.CreateDbContext();
            {
                var persons = context.Person
                    .Include(i => i.Employee)
                    .Include(i => i.Student).ThenInclude(t => t.StudentTestAvailability)
                    .Include(i => i.Student).ThenInclude(t => t.StudentTestTaken)
                    .Where(predicate);
    
                var query = 
                    from p in persons
                    from sth in p.Student.StudentTestHistory
                    from sta in p.Student.StudentTestAvailability
                    where 
                        sth.StudentId == sta.StudentId 
                        && sth.DateTaken == sta.DateAvailable 
                        && sth.TestNameId == sta.TestNameId 
                        && sth.locationId == sta.locationId
                    select new
                    {
                        Person = p,
                        History = sth,
                        Avalability = sta
                    };
    
                if (filter.NameId > 0)
                    query = query.Where(x => x.Avalability.TestNameId == filter.NameId);
                        
                if (filter.LocationId > 0)
                    query = query.Where(x => x.Avalability.LocationId == filter.LocationId);
    
                return await query
                    .Select(x => x.Person)
                    .ToListAsync();
            }   
        }
    }