How to modify type parameter of Expression<Func<???, bool>>?
Asked Answered
Q

3

9

I have an instance of the following:

Expression<Func<IRequiredDate, bool>>

I wish to convert it to an instance of the following, so it can be used to run a query in Entity Framework:

Expression<Func<TModel, bool>>

This will allow me to utilize a generic filtering query to any Model which implements IRequiredDate, eg.:

// In some repository function:
var query = DbContext.Set<Order>()
     .FilterByDateRange(DateTime.Today, DateTime.Today);

var query = DbContext.Set<Note>()
     .FilterByDateRange(DateTime.Today, DateTime.Today);

var query = DbContext.Set<Complaint>()
     .FilterByDateRange(DateTime.Today, DateTime.Today);


// The general purpose function, can filter for any model implementing IRequiredDate
public static IQueryable<TModel> FilterByDate<TModel>(IQueryable<TModel> query, DateTime startDate, DateTime endDate) where TModel : IRequiredDate
{
    // This will NOT WORK, as E/F won't accept an expression of type IRequiredDate, even though TModel implements IRequiredDate
    // Expression<Func<IRequiredDate, bool>> dateRangeFilter = x => x.Date >= startDate && x.Date <= endDate;
    // query = query.Where(dateRangeFilter);

    // This also WON'T WORK, x.Date is compiled into the expression as a member of IRequiredDate instead of TModel, so E/F knocks it back for the same reason:
    // Expression<Func<TModel, bool>> dateRangeFilter = x => x.Date >= startDate && x.Date <= endDate;
    // query = query.Where(dateRangeFilter);

    // All you need is lov.... uh... something like this:
    Expression<Func<IRequiredDate, bool>> dateRangeFilter = x => x.Date >= startDate && x.Date <= endDate;
    Expression<Func<TModel, bool>> dateRangeFilterForType = ConvertExpressionType<IRequiredDate, TModel>(dateRangeFilter); // Must convert the expression from one type to another
    query = query.Where(dateRangeFilterForType) // Ahhhh. this will work.

    return query;
}

public static ConvertExpressionType<TInterface, TModel>(Expression<Func<TInterface, bool>> expression)
where TModel : TInterface // It must implement the interface, since we're about to translate them
{
    Expression<Func<TModel, bool>> newExpression = null;

    // TODO: How to convert the contents of expression into newExpression, modifying the
    // generic type parameter along the way??

    return newExpression;
}

I understand that they are different types and cannot be cast. However I am wondering if there is a way to create a new Expression<Func<TModel, bool>>, then rebuild it based on the contents of the Expression<Func<IRequiredDate, bool>> provided, switching any type references from IRequiredDate to TModel in the process.

Can this be done?

Quoit answered 16/1, 2014 at 6:38 Comment(3)
I'm having trouble understanding your question. Can you provide a concrete example?Florineflorio
Is TModel generic type parameter?Predestinate
I've added the sample code to the question, see above.Quoit
R
11

So the method to actually do the mapping isn't that hard, but sadly there isn't a good way that I can see of generalizing it. Here is a method that takes a Func<T1, TResult> and maps it to a delegate where the parameter is something more derived than T1:

public static Expression<Func<NewParam, TResult>> Foo<NewParam, OldParam, TResult>(
    Expression<Func<OldParam, TResult>> expression)
    where NewParam : OldParam
{
    var param = Expression.Parameter(typeof(NewParam));
    return Expression.Lambda<Func<NewParam, TResult>>(
        expression.Body.Replace(expression.Parameters[0], param)
        , param);
}

This uses the Replace method to replace all instances of one expression with another. The definition is:

internal class ReplaceVisitor : ExpressionVisitor
{
    private readonly Expression from, to;
    public ReplaceVisitor(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    public override Expression Visit(Expression node)
    {
        return node == from ? to : base.Visit(node);
    }
}

public static Expression Replace(this Expression expression,
    Expression searchEx, Expression replaceEx)
{
    return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}

Now we can use this method (which should be given a better name) like so:

Expression<Func<object, bool>> oldExpression = whatever;
Expression<Func<string, bool>> newExpression =
    Foo<string, object, bool>(oldExpression);

And of course since Func is actually covariant with respect to its parameters, we can be sure that any calls to this method generate expressions that won't add runtime failure points.

You could trivially make versions of this for Func<T1, T2, TResult>, and so on and so forth up through the 16 different types of Func if you wanted, just creating a parameter expression for each, and replacing all of the old ones with new ones. It'd be tedious, but just following the pattern. Given that there needs to be a generic argument for both the old and new parameter types though, and that there's no way of inferring the arguments, that'd get...messy.

Radiograph answered 16/1, 2014 at 21:0 Comment(2)
This is pretty much what I asked for - I will give it a go and report the results.Quoit
While @felipe's answer is actually a better solution for my scenario, I will mark this as the answer as this directly addresses the question I asked. However readers should definitely consider felipe's solution as well.Quoit
B
3

Fortunately, for what you want it is not necessary to play with expression trees. What you do need is to enhance the template restriction:

public static IQueryable<TModel> FilterByDate<TModel>(this IQueryable<TModel> src, DateTime startDate, DateTime endDate) where TModel: class, IRequiredDate {
    return src.Where(x => x.Date >= startDate && x.Date <= endDate);
}

A bit of explanation. Using LINQPad you can see that the expression trees generated are different when the class requirement is removed. The Where clause is like this when the restriction is present:

.Where (x => (x => x.Date >= startDate && x.Date <= endDate))

Whereas when the restriction is removed the expression changes as follows:

.Where (x => (x => (((IRequiredDate)x).Date >= startDate) && (((IRequiredDate)x).Date <= endDate)))

The expression tree has some extra casts, which is why in this form Entity Framework tells you it cannot work with instances of type IRequiredDate.

Biconcave answered 16/1, 2014 at 20:59 Comment(4)
Can you provide any insight into why this is the case? It seems strange for the class constraint to have this side effect.Sucy
This solution would be idea for my scenario, I will try it and let you know.Quoit
I am not sure about the cause. I think it is related to the fact that value types need to be boxed in order to be cast into the interface but reference types not. The IL for ((IComparable<string>)" ").CompareTo("") is different for the similar ((IComparable<int>)1).CompareTo(0), the int version requires boxing of the this parameter.Biconcave
felipe this is a great answer and works for my scenario. Unfortunately I'm compelled so mark Servy's response as the answer as addresses the question more directly.Quoit
R
-1

I only had a few minutes so I haven't thought on this deeply. Does this help?

Expression<Func<IList, bool>> exp1 = (list => list.Count > 0);
Expression<Func<string[], bool>> exp2 = (list => exp1.Compile()(list));
Expression<Func<List<int>, bool>> exp3 = (list => exp1.Compile()(list));

I kinda demonstrates what you want I think.

Reinke answered 16/1, 2014 at 6:48 Comment(1)
Thanks for the suggestion, but these expressions end up going to entity framework rather that begin compiled & executed in memory, so this solution will not work.Quoit

© 2022 - 2024 — McMap. All rights reserved.