【问题标题】:Expression Tree Concatenation with LINQ to SQL使用 LINQ to SQL 的表达式树连接
【发布时间】:2014-12-08 21:00:05
【问题描述】:

我正在尝试制作一个灵活的导出器,以从通过 LINQ to SQL 访问的 SQL 数据库中导出信息,从而程序可以动态选择要选择的字段并在服务器端执行所有处理。

最终目标是有一个简单的语句,如:

var result = db.Product.Select(p => selector(p));

其中选择器是动态创建的表达式树,描述要选择的字段。目前,我将每个 db 字段分配给它自己的单独选择器,例如:

Expression<Func<Product, string>> name = p => p.Name; 
Expression<Func<Product, string>> createdAt = p => p.createdAt.ToString();

目前,我正在获取所选字段并尝试从中创建一个连接表达式,该表达式从 select 语句返回逗号分隔的字符串结果,例如:

Expression<
  Func<
   Func<Product, string>,
   Func<Product, string>,
   Func<Product, string>>> combinator = (a, b) => (p) => a(p) + "," + b(p);

// So I can do something like...
Expression<Func<Product, string>> aggregate = p => "";

foreach (var field in fieldsToSelect)
    aggregate = combinator(aggregate, field);

// This would create...
Expression<Func<Products, string>> aggregate = p => a(p) + "," + b(p);

一旦我用很多字段建立了我的选择器,我就可以在语句中执行它,所有的处理都在服务器上完成。但是,我无法正确地创建一个表达式来连接两个子函数,以使结果不是在往返行程后简单地执行以获取结果的 Func:

var joinedSelector = combinator.Compile();
Func<Products, string> result = joinedSelector(firstSelector.Compile(), secondSelector.Compile());
var query  = db.Product.Select(p => result(p)).ToList();

根据我对表达式树的有限理解,这实际上并不会产生一个,因为语句被编译为正常的 Funcs。我看过 Expression.Coalesce() 但不确定它是否是我所追求的(认为它只是“??”)。

我对这类东西还很陌生,如果有任何帮助,我将不胜感激。

即使人们可以想出更好的方法来解决原始问题,我仍然很想解释一下如何实现我正在尝试做的事情,只是为了学习如何使用表达式树。

【问题讨论】:

标签: c# linq-to-sql lambda expression func


【解决方案1】:

因此,您希望创建一个Combine 方法,该方法可以组合两个选择器的结果。为此,我们需要一个接受两个具有相同输入和相同输出的函数的方法,然后是一个接受该输出类型的两个实例并返回一个新值的函数。

该函数需要用一个公共参数替换选择器主体参数的所有实例。然后它将结果函数的两个参数替换为不同选择器的相应主体。然后我们将所有这些都包装在一个 lambda 中。

public static Expression<Func<T, TResult>> Combine
    <T, TIntermediate1, TIntermediate2, TResult>(
this Expression<Func<T, TIntermediate1>> first,
Expression<Func<T, TIntermediate2>> second,
Expression<Func<TIntermediate1, TIntermediate2, TResult>> resultSelector)
{
    var param = Expression.Parameter(typeof(T));
    var body = resultSelector.Body.Replace(
            resultSelector.Parameters[0],
            first.Body.Replace(first.Parameters[0], param))
        .Replace(
            resultSelector.Parameters[1],
            second.Body.Replace(second.Parameters[0], param));
    return Expression.Lambda<Func<T, TResult>>(body, param);
}

这使用以下方法将一个表达式的所有实例替换为另一个:

public static Expression Replace(this Expression expression,
    Expression searchEx, Expression replaceEx)
{
    return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}
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);
    }
}

现在我们可以写如下:

Expression<Func<Product, string>> name = p => p.Name;
Expression<Func<Product, string>> createdAt = p => p.createdAt.ToString();

Expression<Func<Product, string>> aggregate = 
    Combine(name, createdAt, (a, b) => a + "," + b);

它实际上比你的模型更简单,因为结果选择器不需要知道任何关于它的输入是如何生成的,或者它们实际上是选择器。该解决方案还允许每个选择器选择不同的类型,并让它们与结果不同,这仅仅是因为在推断泛型参数时添加所有这些没有实际成本。

有了这个,你甚至可以很容易地聚合任意数量的选择器,因为你已经设置了类型限制:

IEnumerable<Expression<Func<Product, string>>> selectors = new[]{
    name,
    createdAt,
    name,
};

var finalSelector = selectors.Aggregate((first, second) =>
    Combine(first, second, (a, b) => a + "," + b));

例如,这将使您拥有一个params 方法,该方法接受任意数量的选择器(通用输入和输出类型),并能够将它们的所有结果聚合在一起。

【讨论】:

  • 您好,Servy,感谢您的帮助,正是我想要的。给了我一些东西来完全理解它,但它就像一个魅力;)
猜你喜欢
  • 2023-03-09
  • 2016-04-17
  • 1970-01-01
  • 1970-01-01
  • 2016-06-26
  • 1970-01-01
  • 2011-06-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多