5

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'
Community
  • 1
  • 1
Max
  • 849
  • 9
  • 24

1 Answers1

2

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");
Alexey Merson
  • 414
  • 5
  • 12
  • When I include the string `"B.Bc.CreatedUtc"`, I get this error: `Additional information: No property or field 'Bc' exists in type 'List`1'`. It doesn't look like it's exploring nested graphs correctly? – Max Jul 06 '16 at 14:27
  • Yeah, all of my foreign key relationships are collections for this purpose - though they may be 1-1 in the future, so ideally the solution would be able to accommodate both. – Max Jul 06 '16 at 14:51
  • @Max have you included them? (`.Include(...)`) – Mafii Jul 06 '16 at 14:52
  • @Max you have to tell EF to load your dependencies, or they will not be accessable. – Mafii Jul 06 '16 at 14:54
  • If you mean on the incoming original IQueryable, then B is `Include`d but Bc is not. – Max Jul 06 '16 at 14:55
  • @Max then thats why the error is "No property or field 'Bc' exists" – Mafii Jul 06 '16 at 14:55
  • That means that the dynamic library explores objects in a query and not the objects themselves, right? If it can't create the `.Select()` because I didn't `.Include()` an object, that means it can only find objects that are already in the query. – Max Jul 06 '16 at 15:00