【问题标题】:Generating An Expression For An IQueryable<T> [duplicate]为 IQueryable<T> 生成表达式 [重复]
【发布时间】:2016-11-23 13:24:39
【问题描述】:

我正在使用 LINQ->WCF 数据服务->EF,它支持 LINQ 的一个子集,但有一些注意事项。一旦学习了各种技巧和解决方法,我对此没有任何问题,但我想制作一个可重用的表达式生成器,仅比较 DateTimeDate 部分。

对于常规 EF,您可以使用 EntityFunctions.TruncateTime (EFDbFunctions.TruncateTime (EF6+),但这不适用于数据服务。

到目前为止,我的解决方案是反复构建这种混乱的 where 子句:

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

不得不重复写这很讨厌(但它有效),所以我试图创建类似的东西:

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

任何类似的东西都可以,只是简短、甜美和易读 - 我讨厌重复不必要的代码。

结构不是问题,我知道我需要一些包含IQueryable&lt;T&gt;Func&lt;T, DateTime&gt;(或Expression&lt;Func&lt;T, DateTime&gt;&gt;)和DateTime 并返回IQueryable&lt;T&gt; 的东西。

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

我遇到麻烦的地方是使用这个并构建一个可以放入 where 子句的表达式,而不会违反表达式树的限制。我不完全确定如何在不执行 .Where 的情况下获取现有查询并将我自己的 where 语句添加到表达式中,我认为这可能是这里的关键。我想我需要接收一个Expression&lt;Func&lt;T, DateTime&gt;&gt; 并构建一些东西来使用它来添加一个Expression&lt;Func&lt;T, bool&gt;&gt; to the tree and return it as anIQueryable`。

有人对此有一些经验,或者知道我应该阅读哪些文档吗?

这里最大的障碍是您无法将基于语句的 lambda 转换为表达式,并且您无法将不受支持的函数传递给数据服务 EF。这使得所有幼稚的解决方案都变得不可能,据我所知,手动表达式操作。

【问题讨论】:

  • 看看this link
  • 为什么不能在扩展方法中返回第一个 where 表达式?
  • @user2697817 因为 OP 希望它对任何实体都通用且有用
  • @DavidG 我不明白。该方法是通用的,适用于任何具有DateTime 属性的实体。我有一种感觉,我错过了一些明显的东西。
  • @user2697817 去尝试让它工作,你会明白为什么它更复杂!

标签: c# entity-framework linq wcf datetime


【解决方案1】:

这是我在阅读了很多关于该主题的内容后提出的解决方案:

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);
}

它被拆分为多个函数以减少冗余代码并同时支持DateTimeDateTime?

我意识到没有检查可空版本是否缺乏价值 - 这是我很快就会添加的内容,但我想提供解决方案供其他人学习,并确保没有人浪费时间解释这一点对我来说。为了提高效率和可读性,我总是会检查我的代码几次,记录功能,注释不清楚的事情,确保不会出现意外的Exceptions,但这是在此之前。如果您逐字使用此代码,请记住这一点(如果您这样做,请告诉我,我想知道我没有浪费精力发布此代码)。

【讨论】:

  • 很好地让它工作。突然之间,表达看起来还不错的冗长。
  • @user2697817 这……很糟糕。它非常脆弱,因为它高度依赖于表达式的具体实现,而不是足够通用以支持任何任意表达式。它根本不是那么可维护或容易适应其他情况,它相当难以阅读,而且它只是做了很多不必要的事情;它确实需要更多的手动表达式操作来实现这一点。解决更一般的情况实际上更容易
  • @Servy 就像我说的,我还没有清理它。我计划将它概括为接受任何任意表达式,减少冗余表达式等。这只是一个“原型”,如果你愿意的话 - 让它工作而不用担心它是否以最好的方式完成。我以前没有搞过这样的表情构建,所以它很笨重。我想如果我以后不编辑清理后的版本,我会遭受更多的判断吗? :P
【解决方案2】:

您始终可以使用System.Linq.Expressions.Expression 类方法构建所需的表达式。然而,这很烦人、棘手且容易出错。

相反,您可以使用编译时原型表达式,使用我为回答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);
        }
    }
}

这个想法很简单。您使用参数创建原型 lambda 表达式:

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

然后用实际的表达式替换参数。

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

例如,有问题的方法可以这样实现:

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);
    }
}

但是,还有更通用的方法,它也适用于查询语法。您使用自然 LINQ to Objects 样式(使用 CLR 方法/属性/运算符)编写查询,然后使用单个扩展方法将查询转换为 WCF 兼容格式。该方法本身将使用ExpressionVistor 来重写查询表达式。例如,这里是实现DateTime 平等的起点:

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);
        }
    }
}

您可以在需要时相对轻松地添加其他转化。它可以在上述方法中,也可以在链接的帖子中拦截其他Visit 方法。

示例用法:

var query = (from x in myQueryable
             where x.DateProperty == DateToCompare
             ...
             select ...
             ).AsWcfQueryable()

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-10-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-02
    • 1970-01-01
    相关资源
    最近更新 更多