Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 686
Brendan Hill Avatar asked Jan 16 '14 06:01

Brendan Hill


2 Answers

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.

like image 144
Servy Avatar answered Oct 16 '22 03:10

Servy


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.

like image 25
felipe Avatar answered Oct 16 '22 03:10

felipe