从表达式创建动态的Linq select子句 [英] Creating a dynamic Linq select clause from Expressions

查看:83
本文介绍了从表达式创建动态的Linq select子句的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

假设我定义了以下变量:

IQueryable<MyClass> myQueryable;
Dictionary<string, Expression<Func<MyClass, bool>>> extraFields;
// the dictionary is keyed by a field name

现在,我想在IQueryable上添加一些动态字段,以便它返回IQueryable<ExtendedMyClass>,其中ExtendedMyClass定义为:

class ExtendedMyClass
{
  public MyClass MyObject {get; set;}
  public IEnumerable<StringAndBool> ExtraFieldValues {get; set;}
}

class StringAndBool
{
  public string FieldName {get; set;}
  public bool IsTrue {get; set;}
}

换句话说,对于extraFields中的每个值,我想在ExtendedMyClass.ExtraFieldValues中具有一个值,该值表示该行的表达式是否为True.

我觉得这在动态Linq和LinqKit中应该可行,尽管我以前从未认真使用过.我也乐于接受其他建议,尤其是如果可以使用出色的强型Linq来完成.

我正在使用Linq to Entities,因此查询需要转换为SQL.

解决方案

因此,我们将在此处执行很多步骤,但是每个步骤都应相当简短,独立,可重复使用 ,并且相对可以理解.

我们要做的第一件事是创建一个可以组合表达式的方法.它会做的是接受一个输入并生成一个中间值的表达式.然后,将使用第二个表达式,该表达式接受与第一个表达式相同的输入作为中间结果的类型作为输入,然后计算一个新结果.它将使用第一个输入返回一个新表达式,并返回第二个输出.

public static Expression<Func<TFirstParam, TResult>>
    Combine<TFirstParam, TIntermediate, TResult>(
    this Expression<Func<TFirstParam, TIntermediate>> first,
    Expression<Func<TFirstParam, TIntermediate, TResult>> second)
{
    var param = Expression.Parameter(typeof(TFirstParam), "param");

    var newFirst = first.Body.Replace(first.Parameters[0], param);
    var newSecond = second.Body.Replace(second.Parameters[0], param)
        .Replace(second.Parameters[1], newFirst);

    return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
}

为此,我们只需将第二个表达式主体中第二个参数的所有实例替换为第一个表达式主体即可.我们还需要确保两个实现对主参数使用相同的参数实例.

此实现需要一种方法来将一个表达式的所有实例替换为另一个:

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

public static Expression Replace(this Expression expression,
    Expression searchEx, Expression replaceEx)
{
    return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}

接下来,我们将编写一个方法,该方法接受一系列表达式,这些表达式接受相同的输入并计算相同类型的输出.它将转换为接受相同输入的单个表达式,但计算输出的序列作为结果,其中序列中的每个项目代表每个输入表达式的结果. /p>

此实现非常简单;我们创建一个新的数组,使用每个表达式的主体(用一致的表达式替换参数)作为数组中的每个项目.

public static Expression<Func<T, IEnumerable<TResult>>> AsSequence<T, TResult>(
    this IEnumerable<Expression<Func<T, TResult>>> expressions)
{
    var param = Expression.Parameter(typeof(T));
    var body = Expression.NewArrayInit(typeof(TResult),
        expressions.Select(selector =>
            selector.Body.Replace(selector.Parameters[0], param)));
    return Expression.Lambda<Func<T, IEnumerable<TResult>>>(body, param);
}

现在我们已经摆脱了所有这些通用辅助方法,我们可以开始处理您的特定情况.

这里的第一步是将字典变成一系列表达式,每个表达式接受一个MyClass并创建一个表示该对的StringAndBool.为此,我们将在字典的值上使用Combine,然后使用lambda作为第二个表达式,除了关闭该键对的键之外,还使用它的中间结果来计算StringAndBool对象.

IEnumerable<Expression<Func<MyClass, StringAndBool>>> stringAndBools =
    extraFields.Select(pair => pair.Value.Combine((foo, isTrue) =>
        new StringAndBool()
        {
            FieldName = pair.Key,
            IsTrue = isTrue
        }));

现在,我们可以使用AsSequence方法将其从选择器序列转换为选择一个序列的单个选择器:

Expression<Func<MyClass, IEnumerable<StringAndBool>>> extrafieldsSelector =
    stringAndBools.AsSequence();

现在我们快完成了.现在,我们只需要在此表达式上使用Combine来写出我们的lambda,以便将MyClass选择为ExtendedMyClass,同时使用先前生成的选择器来选择其他字段:

var finalQuery = myQueryable.Select(
    extrafieldsSelector.Combine((foo, extraFieldValues) =>
        new ExtendedMyClass
        {
            MyObject = foo,
            ExtraFieldValues = extraFieldValues,
        }));


我们可以采用相同的代码,删除中间变量,并依靠类型推断将其下拉至单个语句,前提是您不会发现它太过笨拙:

var finalQuery = myQueryable.Select(extraFields
    .Select(pair => pair.Value.Combine((foo, isTrue) =>
        new StringAndBool()
        {
            FieldName = pair.Key,
            IsTrue = isTrue
        }))
    .AsSequence()
    .Combine((foo, extraFieldValues) =>
        new ExtendedMyClass
        {
            MyObject = foo,
            ExtraFieldValues = extraFieldValues,
        }));

值得注意的是,这种通用方法的主要优势在于,使用更高级别的Expression方法所产生的代码至少可以合理地理解,而且可以在编译时静态验证 ,请输入安全性.这里有几种通用,可重用,可测试,可验证的扩展方法,这些方法一旦编写,就使我们能够完全通过方法和lambda的组合来解决问题,并且不需要任何实际的表达式操作,这两者都是复杂,容易出错,并消除了所有类型的安全性.这些扩展方法中的每一个都以这样的方式设计:只要输入表达式有效,结果表达式将始终有效,并且此处的输入表达式都是lambda表达式,因此已知所有输入表达式都是有效的,编译器将对其进行验证为了类型安全.

Let's say I have defined the following variables:

IQueryable<MyClass> myQueryable;
Dictionary<string, Expression<Func<MyClass, bool>>> extraFields;
// the dictionary is keyed by a field name

Now, I want to tack on some dynamic fields to the IQueryable, so that it returns an IQueryable<ExtendedMyClass>, where ExtendedMyClass is defined as:

class ExtendedMyClass
{
  public MyClass MyObject {get; set;}
  public IEnumerable<StringAndBool> ExtraFieldValues {get; set;}
}

class StringAndBool
{
  public string FieldName {get; set;}
  public bool IsTrue {get; set;}
}

In other words, for every value in extraFields, I want to have a value in ExtendedMyClass.ExtraFieldValues representing whether that expression evaluates to True or not for that row.

I have a feeling this should be doable in dynamic Linq and LinqKit, though I've never used that seriously before. I'm also open to other suggestions, especially if this can be done in good ol' strong-typed Linq.

I am using Linq to Entities, so the query needs to translate to SQL.

解决方案

So, we'll have a lot of steps here, but each individual step should be fairly short, self-contained, reusable, and relatively understandable.

The first thing we'll do is create a method that can combine expressions. What it will do is take an expression that accepts some input and generates an intermediate value. Then it will take a second expression that accepts, as input, the same input as the first, the type of the intermediate result, and then computes a new result. It will return a new expression taking the input of the first, and returning the output of the second.

public static Expression<Func<TFirstParam, TResult>>
    Combine<TFirstParam, TIntermediate, TResult>(
    this Expression<Func<TFirstParam, TIntermediate>> first,
    Expression<Func<TFirstParam, TIntermediate, TResult>> second)
{
    var param = Expression.Parameter(typeof(TFirstParam), "param");

    var newFirst = first.Body.Replace(first.Parameters[0], param);
    var newSecond = second.Body.Replace(second.Parameters[0], param)
        .Replace(second.Parameters[1], newFirst);

    return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
}

To do this we simply replace all instances of the second parameter in the second expression's body with the body of the first expression. We also need to ensure both implementations use the same parameter instance for the main parameter.

This implementation requires having a method to replace all instances of one expression with another:

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

public static Expression Replace(this Expression expression,
    Expression searchEx, Expression replaceEx)
{
    return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}

Next we'll write a method that accepts a sequences of expressions that accept the same input and compute the same type of output. It will transform this into a single expression that accepts the same input, but computes a sequence of the output as a result, in which each item in the sequence represents the result of each of the input expressions.

This implementation is fairly straightforward; we create a new array, use the body of each expression (replacing the parameters with a consistent one) as each item in the array.

public static Expression<Func<T, IEnumerable<TResult>>> AsSequence<T, TResult>(
    this IEnumerable<Expression<Func<T, TResult>>> expressions)
{
    var param = Expression.Parameter(typeof(T));
    var body = Expression.NewArrayInit(typeof(TResult),
        expressions.Select(selector =>
            selector.Body.Replace(selector.Parameters[0], param)));
    return Expression.Lambda<Func<T, IEnumerable<TResult>>>(body, param);
}

Now that we have all of these general purpose helper methods out of the way, we can start working on your specific situation.

The first step here is to turn your dictionary into a sequence of expressions, each accepting a MyClass and creating a StringAndBool that represents that pair. To do this we'll use Combine on the value of the dictionary, and then use a lambda as the second expression to use it's intermediate result to compute a StringAndBool object, in addition to closing over the pair's key.

IEnumerable<Expression<Func<MyClass, StringAndBool>>> stringAndBools =
    extraFields.Select(pair => pair.Value.Combine((foo, isTrue) =>
        new StringAndBool()
        {
            FieldName = pair.Key,
            IsTrue = isTrue
        }));

Now we can use our AsSequence method to transform this from a sequence of selectors into a single selector that selects out a sequence:

Expression<Func<MyClass, IEnumerable<StringAndBool>>> extrafieldsSelector =
    stringAndBools.AsSequence();

Now we're almost done. We now just need to use Combine on this expression to write out our lambda for selecting a MyClass into an ExtendedMyClass while using the previous generated selector for selecting out the extra fields:

var finalQuery = myQueryable.Select(
    extrafieldsSelector.Combine((foo, extraFieldValues) =>
        new ExtendedMyClass
        {
            MyObject = foo,
            ExtraFieldValues = extraFieldValues,
        }));


We can take this same code, remove the intermediate variable and rely on type inference to pull it down to a single statement, assuming you don't find it too unweidly:

var finalQuery = myQueryable.Select(extraFields
    .Select(pair => pair.Value.Combine((foo, isTrue) =>
        new StringAndBool()
        {
            FieldName = pair.Key,
            IsTrue = isTrue
        }))
    .AsSequence()
    .Combine((foo, extraFieldValues) =>
        new ExtendedMyClass
        {
            MyObject = foo,
            ExtraFieldValues = extraFieldValues,
        }));

It's worth noting that a key advantage of this general approach is that the use of the higher level Expression methods results in code that is at least reasonably understandable, but also that can be statically verified, at compile time, to be type safe. There are a handful of general purpose, reusable, testable, verifiable, extension methods here that, once written, allows us to solve the problem purely through composition of methods and lambdas, and that doesn't require any actual expression manipulation, which is both complex, error prone, and removes all type safety. Each of these extension methods is designed in such a way that the resulting expression will always be valid, so long as the input expressions are valid, and the input expressions here are all known to be valid as they are lambda expressions, which the compiler verifies for type safety.

这篇关于从表达式创建动态的Linq select子句的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆