1

Suppose that I have some C# code that looks like this:

var query1 = query.Where(x => x.BirthDate > now);
var query2 = query.Where(x => x.EnrollmentDate > now);
var query3 = query.Where(x => x.GraduationDate > now);

The actual code is more complicated, but I'm using a simple example. I'm passing this code to Entity Framework.

Suppose I look at this and say, "This is un-DRY", and then I write a function like this.

public IQueryable<Student> FilterAfterDate(IQueryable<Student> query,
    Expression<Func<Student, DateTime>> GetDateExpression, DateTime now)
{
    return query.Where(x => GetDateExpression(x) > now);
}

At runtime, this produces the following error:

The LINQ expression node type 'Invoke' is not supported in LINQ to Entities.

I don't know the particulars, but I believe that the solution to this probably is to somehow take my FilterAfterDate which is Expression<Func<Student, DateTime>> and somehow combine that with the datetime comparison to produce an expression of type Expression<Func<Student, bool>> to pass to the Where function, but I don't know how to do this.

Vivian River
  • 28,530
  • 54
  • 179
  • 298
  • Thanks, @KirkWoll. I fixed it. – Vivian River Apr 03 '19 at 21:58
  • But long story short, you're going to have to do some expression tree manipulation. I have a library for composing expressions for EF mapping, though it doesn't apply to your particular requirement. Here's a [salient class](https://github.com/kswoll/mapit/blob/master/MapIt/Utils/LambdaBinder.cs) though you'd need to look at some of the ancillary files to get a full picture. – Kirk Woll Apr 03 '19 at 21:59
  • [LINQKit](https://github.com/scottksmith95/LINQKit) is the most common way to handle this, besides rolling your own. My EF implementation of [full outer join](https://stackoverflow.com/a/49418695/2557128) includes a dedicated `Invoke` replacement for EF, but it is wrapped up in the join code and probably hard to follow. I can post something simpler. – NetMage Apr 03 '19 at 22:31
  • That sounds like what I'm looking for. Too bad such a seemingly simple task should require such a thing. Would you like to provide an answer showing how to do what I want using LINQKit? – Vivian River Apr 03 '19 at 22:33
  • I prefer rolling my own, though I am thinking if you have to pass `now` to your `FilterAfterDate`, you aren't really gaining anything in this simple example over the original code, just replacing the `>` with `,`. – NetMage Apr 03 '19 at 22:35

2 Answers2

0

Using LINQKit, you can write your method (I prefer as an extension method) like so:

public static class StudentExt {
    public static IQueryable<Student> FilterAfterDate(this IQueryable<Student> query,
        Expression<Func<Student, DateTime>> GetDateExpression, DateTime now)
        => query.AsExpandable().Where(x => GetDateExpression.Invoke(x) > now);
}

And use it like so:

var q1 = query.FilterAfterDate(q => q.BirthDate, now);
var q2 = query.FilterAfterDate(q => q.EnrollmentDate, now);
var q3 = query.FilterAfterDate(q => q.GraduationDate, now);

To roll your own, you just use a common ExpressionVisitor that does a replace:

public static class ExpressionExt {
    /// <summary>
    /// Replaces an Expression (reference Equals) with another Expression
    /// </summary>
    /// <param name="orig">The original Expression.</param>
    /// <param name="from">The from Expression.</param>
    /// <param name="to">The to Expression.</param>
    /// <returns>Expression with all occurrences of from replaced with to</returns>
    public static Expression Replace(this Expression orig, Expression from, Expression to) => new ReplaceVisitor(from, to).Visit(orig);
}

/// <summary>
/// ExpressionVisitor to replace an Expression (that is Equals) with another Expression.
/// </summary>
public class ReplaceVisitor : ExpressionVisitor {
    readonly Expression from;
    readonly Expression to;

    public ReplaceVisitor(Expression from, Expression to) {
        this.from = from;
        this.to = to;
    }

    public override Expression Visit(Expression node) => node == from ? to : base.Visit(node);
}

Now, with Replace available, you can create a lambda template and use it to substitute in the Expression parameter:

public static class StudentExt {
    public static IQueryable<Student> FilterAfterDate(this IQueryable<Student> query,
        Expression<Func<Student, DateTime>> GetDateExpression, DateTime now) {
        Expression<Func<DateTime,bool>> templateFn = x => x > now;
        var filterFn = Expression.Lambda<Func<Student,bool>>(templateFn.Body.Replace(templateFn.Parameters[0], GetDateExpression.Body), GetDateExpression.Parameters);

        return query.Where(filterFn);
    }
}

And you use it the same as with LINQKit.

NetMage
  • 22,242
  • 2
  • 28
  • 45
-1

Here's my solution:

public static class MyExtensions
{
    public static MethodInfo GetMethod<TSource, TResult>(this Expression<Func<TSource, TResult>> lambda)
        => (lambda?.Body as MethodCallExpression)?.Method ?? throw new ArgumentException($"Not a {nameof(MethodCallExpression)}.");

    private static readonly MethodInfo _miWhere = GetMethod((IQueryable<int> q) => q.Where(x => false)).GetGenericMethodDefinition();

    public static IQueryable<TSource> WhereGreaterThan<TSource, TCompare>(this IQueryable<TSource> source, Expression<Func<TSource, TCompare>> selector, Expression<Func<TCompare>> comparand)
    {
        var predicate = Expression.Lambda<Func<TSource, bool>>(Expression.GreaterThan(selector.Body, comparand.Body), selector.Parameters[0]);
        var where = Expression.Call(_miWhere.MakeGenericMethod(typeof(TSource)), source.Expression, predicate);
        return source.Provider.CreateQuery<TSource>(where);
    }
}

Test:

var now = DateTime.Now;
var result = new[]
    {
        new { Item = "Past", Date = now.AddDays(-1) },
        new { Item = "Future", Date = now.AddDays(1) }
    }
    .AsQueryable()
    .WhereGreaterThan(x => x.Date, () => now)
    .Select(x => x.Item)
    .Single();
// "Future"
tinudu
  • 1,063
  • 9
  • 19