Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically Build Include with Where Filter

Background

I need to be able to build a .Include that filters results down to records created on or before a specific date. I will not know the types of the objects on the original LINQ statement, and I will be finding them (through a whole lot of hoops, but that is working).

I have a graph of objects like this:

          A -> Ac
         /  \
  Bc <- B    \
       /      \  
Cc <- C        D -> Dc

Objects Ac, Bc, Cc, and Dc are all found dynamically (that is, a developer does not type in these relationships when writing the original linq statement). These then need to be added to an IQueryable expression and only return values up to a specific DateTime.

The Code So Far

I tried building a function that constructs a lambda and compares dates. So far, if the original query is A.Include(x => x.B).Include(x => x.B.Select(y => y.C), I can construct the string "A.B.Bc", so I know the types of A, B, and Bc, and the way they relate.

private static IQueryable<T> FilterChangeTrackerToDate<T>(this IQueryable<T> query, string includeString, DateTime targetDateTime)
{
    var param = Expression.Parameter( typeof( T ), "x" );

    var prop = Expression.Property(param, includeString + ".CreatedUtc");

    var dateConst = Expression.Constant(targetDateTime);

    var compare = Expression.LessThanOrEqual(prop, dateConst);

    var lambda = Expression.Lambda<Func<T, bool>>(compare, param);

    return query.Where(lambda);
}

The issue is that the above code crashes because "A.B.Bc.CreatedUtc" isn't the correct way to load the date property - I need to traverse this through .Select statements.

The Problem

In order to both load the records and filter them, I need to dynamically build a .Select to pull out the values I need (which, thankfully, are static based on inheritance) and put those values into an anonymous type, which can then be filtered with a .Where(). All of this needs to be done with minimal generics, as I don't have compile-time types to validate against and I'd have to abuse reflection to get them to work.

Essentially, I need to create this:

.Select( x => new
{
    Bc = x.B.Select(z => z.Bc.Select(y => y.CreatedUtc).Where( q => q > DateTime.UtcNow ) ),
    A= x
} )

dynamically, using the string "A.B.Bc", for any level of nesting.

Resources

This is where I saw how to filter an EF include method: LINQ To Entities Include + Where Method

This post talks about dynamically creating a Select, but it is only selecting top-level values and does not seem like it can build the rest of the parts needed for my problem: How to create LINQ Expression Tree to select an anonymous type

Dynamic Linq

Using the System.Linq.Dynamic library, I have tried to access the CreatedUtc value off of Bc:

query = (IQueryable<T>) query.Select(includeString + ".CreatedUtc");

Where includeString = "B.Bc", but this gives me an exception of:

Exception thrown: 'System.Linq.Dynamic.ParseException' in System.Linq.Dynamic.dll

Additional information: No property or field 'Bc' exists in type 'List`1'
like image 852
Max Avatar asked Oct 31 '22 00:10

Max


1 Answers

I believe System.Linq.Dynamic is what you need. It's a library originally written by ScottGu from Microsoft, and it allows you to build LINQ query from string like that:

IQueryable nestedQuery = query.Select("B.Bc.CreatedUtc");

where query is IQueryable<A>.

There is a number of forks of this library. You can start from here. Documentation on query language is here. Also here there is a version with some additional functionality and here is dotnet.core version.

UPD1: This library lacks of SelectMany extension method but this one has. So with latter library you can do the following:

query.SelectMany("B").SelectMany("Bc").Select("CreatedUtc");
like image 139
Alexey Merson Avatar answered Nov 15 '22 05:11

Alexey Merson