Expression Tree Concatenation with LINQ to SQL

半世苍凉 提交于 2019-12-12 02:55:40

问题


I'm having a go at making a flexible exporter to export info from a SQL db accessed via LINQ to SQL, whereby the program can dynamically choose which fields to select and do all the processing server side.

The final aim is to have a simple statement like:

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

Where selector is a dynamically created Expression Tree describing the fields to select. Currently, I have each of the db fields assigned to it's own individual selector like:

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

At the moment, I'm taking the chosen fields and trying to make one concatenated expression out of them that returns a comma delimited string result from the select statement, something like:

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

Once I've built up my selector with however many fields, I can execute it in the statement and all the processing is done on the server. However, I've been unable to properly create an expression to concatenate the two child functions in such a manner that the result isn't a Func that's simply executed after the round trip to fetch the results:

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

From my limited understanding of Expression Trees, this doesn't actually result in one as the statements are compiled to normal Funcs. I've looked at Expression.Coalesce() but am not sure if it's what I'm after (think it just does "??").

I'm pretty new to this sort of stuff, and would appreciate any help.

Even if people can think of better ways to attack the original problem, I'd still quite like an explanation of how to achieve what I'm trying to do just for the sake of learning how to use Expression Trees.


回答1:


So you're looking to create a Combine method that can combine the results of two selectors. To do this we'll need a method that accepts two functions with the same input and same output, and then a function accepting two instances of that output type and returning a new value.

The function will need to replace all instances of the parameters of the selectors' body with a common parameter. It will then replace the two parameters of the result function with the corresponding bodies of the different selectors. Then we just wrap all of that up in a 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);
}

This uses the following method to replace all instances of one expression with another:

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

Now we can write the following:

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

It actually comes out a bit simpler than in your mockup, as the result selector doesn't need to know anything about how its inputs are generated, or that they're actually selectors. This solution also allows for each selector to select out different types, and for them to differ from the result, simply because there's no real cost to adding all of this when the generic arguments are just going to be inferred.

And with this in place you can even easily aggregate an arbitrary number of selectors, given the type restrictions you've put in place:

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

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

This would let you, for example, have a params method accepting any number of selectors (of a common input and output type) and be able to aggregate all of their results together.



来源:https://stackoverflow.com/questions/27366807/expression-tree-concatenation-with-linq-to-sql

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!