简体   繁体   中英

How to use one C# expression inside another C# expression for Entity Framework?

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.

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.

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"

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM