Expression.Call GroupBy然后选择和Count()?

时间:2016-01-26 17:20:18

标签: c# linq expression-trees

使用表达式树,我需要以通用方式构建GroupBy。 我将使用的静态方法如下:

public static IQueryable<Result> GroupBySelector<TSource>(this IQueryable<TSource> source, String coloumn)
{

  //Code here

}

Result类有两个属性:

public string Value { get; set; }
public int Count { get; set; }

基本上我想通过表达式树构建以下Linq查询:

query.GroupBy(s => s.Country).Select(p => new 
                {
                    Value = p.Key,
                    Count = p.Count()
                }
            )

你会如何实现它?

1 个答案:

答案 0 :(得分:5)

看着:

query.GroupBy(s => s.Country).Select(p => new 
  {
    Value = p.Key,
    Count = p.Count()
  }
);

要匹配IQueryable<Result>的签名,您实际需要的是:

query.GroupBy(s => s.Country).Select(p => new 
  Result{
    Value = p.Key,
    Count = p.Count()
  }
);

现在,Select可以与任何IQueryable<IGrouping<string, TSource>>一起使用。只有GroupBy才需要我们使用表达式树。

我们的任务是从一个表示属性的类型和字符串开始(它本身返回字符串)并创建一个表示获取该属性值的Expression<Func<TSource, string>>

所以,让我们先生成方法的简单部分:

public static IQueryable<Result> GroupBySelector<TSource>(this IQueryable<TSource> source, string column)
{
    Expression<Func<TSource, string>> keySelector = //Build tree here.

    return source.GroupBy(keySelector).Select(p => new Result{Value = p.Key, Count = p.Count()});
}

好。如何构建树。

我们需要一个类型为TSource的参数的lambda:

var param = Expression.Parameter(typeof(TSource));

我们需要获取名称与column匹配的属性:

Expression.Property(param, column);

lambda中唯一需要的逻辑就是访问该属性:

Expression<Func<TSource, string>> keySelector = Expression.Lambda<Func<TSource, string>>
(
  Expression.Property(param, column),
  param
);

全部放在一起:

public static IQueryable<Result> GroupBySelector<TSource>(this IQueryable<TSource> source, String column)
{
    var param = Expression.Parameter(typeof(TSource));
    Expression<Func<TSource, string>> keySelector = Expression.Lambda<Func<TSource, string>>
    (
        Expression.Property(param, column),
        param
    );
    return source.GroupBy(keySelector).Select(p => new Result{Value = p.Key, Count = p.Count()});
}

关于唯一剩下的就是异常处理,我通常不会在答案中包含,但其中一部分值得关注。

首先是明显的空和空检查:

public static IQueryable<Result> GroupBySelector<TSource>(this IQueryable<TSource> source, String column)
{
    if (source == null) throw new ArgumentNullException("source");
    if (column == null) throw new ArgumentNullException("column");
    if (column.Length == 0) throw new ArgumentException("column");
    var param = Expression.Parameter(typeof(TSource));
    Expression<Func<TSource, string>> keySelector = Expression.Lambda<Func<TSource, string>>
    (
        Expression.Property(param, column),
        param
    );
    return source.GroupBy(keySelector).Select(p => new Result{Value = p.Key, Count = p.Count()});
}

现在,让我们考虑如果我们传递的column字符串与TSource的属性不匹配会发生什么。我们得到ArgumentException消息Instance property '[Whatever you asked for]' is not defined for type '[Whatever the type is]'.这就是我们在这种情况下想要的,所以没问题。

但是,如果我们传递了一个确定属性的字符串但该属性不属于string类型的字符串,那么我们会得到类似"Expression of type 'System.Int32' cannot be used for return type 'System.String'"的内容。这不是可怕的,但它也不是很好。让我们更明确一点:

public static IQueryable<Result> GroupBySelector<TSource>(this IQueryable<TSource> source, String column)
{
    if (source == null) throw new ArgumentNullException("source");
    if (column == null) throw new ArgumentNullException("column");
    if (column.Length == 0) throw new ArgumentException("column");
    var param = Expression.Parameter(typeof(TSource));
    var prop = Expression.Property(param, column);
    if (prop.Type != typeof(string)) throw new ArgumentException("'" + column + "' identifies a property of type '" + prop.Type + "', not a string property.", "column");
    Expression<Func<TSource, string>> keySelector = Expression.Lambda<Func<TSource, string>>
    (
        prop,
        param
    );
    return source.GroupBy(keySelector).Select(p => new Result{Value = p.Key, Count = p.Count()});
}

如果这个方法是内部的,那么上面的内容可能会被过度杀死,但是如果它是公开的,那么如果你来调试它,那么额外的信息将非常值得。