Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generating An Expression For An IQueryable<T> [duplicate]

I'm using LINQ->WCF Data Services->EF, which supports a subset of LINQ with a few caveats. I've had no trouble with that once learning the tricks and workarounds for various things, but I'd like to make a reusable expression generator for comparing only the Date portion of a DateTime.

With regular EF you can use EntityFunctions.TruncateTime (EF<6) or DbFunctions.TruncateTime (EF6+), but this doesn't work over data services.

My solution so far has been to repeatedly build this mess of a where clause:

.Where(x => x.DateProperty.Year == DateToCompare.Year && 
            x.DateProperty.Month == DateToCompare.Month && 
            x.DateProperty.Day == DateToCompare.Day);

That's just nasty to have to repeatedly write (but it works), so I was trying to create something like:

.WhereDate(x => x.DateProperty, DateToCompare);

Anything similar would do, just short and sweet and readable - I detest repetitive unnecessary-feeling code.

The structure isn't a problem, I know I need something that takes IQueryable<T>, Func<T, DateTime> (or Expression<Func<T, DateTime>>), and DateTime and returns IQueryable<T>.

public static IQueryable<T> WhereDate<T>(this IQueryable<T> data, Func<T, DateTime>> selector, DateTime date)
{
    return data.Where(/*Something*/);
};

Where I'm having trouble is taking this and building an expression that can be put into that where clause without violating the restrictions of expression trees. I'm not entirely sure how to take an existing query and add my own where statement to the expression without doing a .Where, which I think might be the key here. I think I need to take in an Expression<Func<T, DateTime>> and build something that uses that to add an Expression<Func<T, bool>> to the tree and return it as anIQueryable`.

Anybody got some experience with this, or know which docs I should be reading?

The biggest barriers here are that you can't turn a statement-based lambda into an expression, and you can't pass unsupported functions into the data service or EF. This makes all of the naïve solutions impossible, and as far as I know leaves manual expression manipulation.

like image 384
Yushatak Avatar asked Mar 11 '23 14:03

Yushatak


1 Answers

Here's the solution I came up with after reading a lot about the topic:

private static IQueryable<T> _whereDate<T>(this IQueryable<T> data, MemberExpression date1Expression, ParameterExpression parameter, DateTime date)
{
    var date1Year = Expression.Property(date1Expression, "Year");
    var date1Month = Expression.Property(date1Expression, "Month");
    var date1Day = Expression.Property(date1Expression, "Day");
    var date2Year = Expression.Constant(date.Year);
    var date2Month = Expression.Constant(date.Month);
    var date2Day = Expression.Constant(date.Day);
    var yearsEqual = Expression.Equal(date1Year, date2Year);
    var monthsEqual = Expression.Equal(date1Month, date2Month);
    var daysEqual = Expression.Equal(date1Day, date2Day);
    var allPartsEqual = Expression.AndAlso(Expression.AndAlso(daysEqual, monthsEqual), yearsEqual); //Day->Month->Year to efficiently remove as many as possible as soon as possible.
    var whereClause = Expression.Call(typeof(Queryable), "Where", new Type[] { data.ElementType }, data.Expression, Expression.Lambda(allPartsEqual, parameter));
    return data.Provider.CreateQuery<T>(whereClause);
}

public static IQueryable<T> WhereDate<T>(this IQueryable<T> data, Expression<Func<T, DateTime?>> selector, DateTime date)
{
    var selectorMemberExpression = ((MemberExpression)selector.Body);
    var nullableDateProperty = (PropertyInfo)selectorMemberExpression.Member;
    var entityExpression = Expression.Parameter(typeof(T));
    var date1Expression = Expression.Property(entityExpression, nullableDateProperty);
    return data._whereDate(Expression.PropertyOrField(date1Expression, "Value"), entityExpression, date);
}

public static IQueryable<T> WhereDate<T>(this IQueryable<T> data, Expression<Func<T, DateTime>> selector, DateTime date)
{
    var selectorMemberExpression = ((MemberExpression)selector.Body);
    var dateProperty = (PropertyInfo)selectorMemberExpression.Member;
    var entityExpression = Expression.Parameter(typeof(T));
    return data._whereDate(Expression.Property(entityExpression, dateProperty), entityExpression, date);
}

It's split into multiple functions to reduce redundant code and support both DateTime and DateTime?.

I realize there's no check for a lack of value on the nullable version - that's something I'll add soon enough, but I wanted to get the solution up for anybody else to learn from and make sure nobody wastes their time explaining this to me. I always go through my code a few times for efficiency and readability, documenting the functions, commenting unclear things, making sure no unexpected Exceptions can arise, but this is prior to that. Just keep that in mind if you use this code verbatim (and if you do, let me know, I'd like to know I didn't waste the effort of posting this).

like image 157
Yushatak Avatar answered Mar 20 '23 08:03

Yushatak