4

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.

Yushatak
  • 741
  • 1
  • 5
  • 15

2 Answers2

5

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).

Yushatak
  • 741
  • 1
  • 5
  • 15
  • 1
    Well done for getting it working. Suddenly that verbose where expression isn't looking too bad. – user2697817 Nov 23 '16 at 15:49
  • 1
    @user2697817 It's...pretty bad. It's quite fragile, in that it's highly dependant on the specific implementation of the expression, rather than being general enough to support any arbitrary expression. It's simply not that maintainable or easily adaptable to other situations, it's rather hard to read, and it just does a fair number of things unnecessarily; it does way more manual expression manipulation that is actually needed to accomplish this. Solving the more general case is actually *easier*. – Servy Nov 23 '16 at 16:45
  • 1
    @Servy Like I said, I haven't cleaned it up. I plan to generalize it to accept any arbitrary expression, reduce redundant expressions, etc.. This was just a "prototype", if you will - getting it working without worrying about whether it was done the best way yet. I've not mucked with expression building like this before, so it's clunky. I guess if I don't edit the cleaned-up version in later on I'll suffer more judgement? :P – Yushatak Nov 23 '16 at 16:47
2

You can always build the required expression using the System.Linq.Expressions.Expression class methods. However it's annoying, tricky and error prone.

Instead, you can use a compile time prototype expressions, replacing the parameters with actual values using a small helper utility I've created for my answer to Entity Framework + DayOfWeek:

public static class ExpressionUtils
{
    public static Expression<Func<TResult>> Expr<TResult>(Expression<Func<TResult>> e) => e;
    public static Expression<Func<T, TResult>> Expr<T, TResult>(Expression<Func<T, TResult>> e) => e;
    public static Expression<Func<T1, T2, TResult>> Expr<T1, T2, TResult>(Expression<Func<T1, T2, TResult>> e) => e;
    public static Expression<Func<T1, T2, T3, TResult>> Expr<T1, T2, T3, TResult>(Expression<Func<T1, T2, T3, TResult>> e) => e;
    public static Expression<Func<T1, T2, T3, T4, TResult>> Expr<T1, T2, T3, T4, TResult>(Expression<Func<T1, T2, T3, T4, TResult>> e) => e;
    public static Expression WithParameters(this LambdaExpression expression, params Expression[] values)
    {
        return expression.Parameters.Zip(values, (p, v) => new { p, v })
            .Aggregate(expression.Body, (e, x) => e.ReplaceParameter(x.p, x.v));
    }
    public static Expression ReplaceParameter(this Expression expression, ParameterExpression source, Expression target)
    {
        return new ParameterReplacer { Source = source, Target = target }.Visit(expression);
    }
    class ParameterReplacer : ExpressionVisitor
    {
        public ParameterExpression Source;
        public Expression Target;
        protected override Expression VisitParameter(ParameterExpression node)
        {
            return node == Source ? Target : base.VisitParameter(node);
        }
    }
}

The idea is simple. You create a prototype lambda expression using parameters:

var protoExpr = ExpressionUtils.Expr((DateTime x, DateTime y) =>
    x.Year == y.Year && x.Month == y.Month && x.Day == y.Day);

and then replace the parameters with actual expressions.

var actualExpr = protoExpr.WithParameters(expr1, expr2);

For instance, the method in question could be implemented like this:

public static class WcfQueryableExtensions
{
    public static IQueryable<T> WhereEqual<T>(this IQueryable<T> source, Expression<Func<T, DateTime>> selector, DateTime date)
    {
        var dateValue = ExpressionUtils.Expr(() => date).Body;
        var predicate = Expression.Lambda<Func<T, bool>>(
            ExpressionUtils.Expr((DateTime x, DateTime y) =>
                x.Year == y.Year && x.Month == y.Month && x.Day == y.Day)
                .WithParameters(selector.Body, dateValue),
            selector.Parameters);
        return source.Where(predicate);
    }
}

However, there is more general approach, which also works for query syntax. You write the queries using the natural LINQ to Objects style (with CLR methods/properties/operators), and then use a single extension method to convert the query to a WCF compatible format. The method itself will use ExpressionVistor for rewriting the query expression. For instance, here is the staring point implementing DateTime equality:

public static class WcfQueryableExtensions
{
    public static IQueryable<T> AsWcfQueryable<T>(this IQueryable<T> source)
    {
        var expression = new WcfConverter().Visit(source.Expression);
        if (expression == source.Expression) return source;
        return source.Provider.CreateQuery<T>(expression);
    }

    class WcfConverter : ExpressionVisitor
    {
        protected override Expression VisitBinary(BinaryExpression node)
        {
            if (node.NodeType == ExpressionType.Equal && node.Left.Type == typeof(DateTime))
                return ExpressionUtils.Expr((DateTime x, DateTime y) =>
                    x.Year == y.Year && x.Month == y.Month && x.Day == y.Day)
                    .WithParameters(Visit(node.Left), Visit(node.Right));
            return base.VisitBinary(node);
        }
    }
}

You can relatively easy add other conversions once you need them. It could be inside the above method or by intercepting other Visit methods as in the linked post.

Sample usage:

var query = (from x in myQueryable
             where x.DateProperty == DateToCompare
             ...
             select ...
             ).AsWcfQueryable()
Community
  • 1
  • 1
Ivan Stoev
  • 195,425
  • 15
  • 312
  • 343