从表达式中创建动态Linq select子句

本文关键字:Linq select 子句 动态 创建 表达式 | 更新日期: 2023-09-27 18:00:52

假设我定义了以下变量:

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中应该是可行的,尽管我以前从未认真使用过。我也对其他建议持开放态度,特别是如果这可以在好的ol‘strong类型的Linq中完成的话。

我使用的是Linq-to-Entities,所以查询需要转换为SQL。

从表达式中创建动态Linq select子句

因此,我们在这里有很多步骤,但每个单独的步骤都应该相当短,独立,可重复使用,并且相对容易理解。

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

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

接下来,我们将编写一个方法,该方法接受接受相同输入并计算相同类型输出的表达式序列。它将把它转换为接受相同输入的单个表达式,但计算输出的序列作为结果,其中序列中的每个项表示每个输入表达式的结果。

这种实现相当简单;我们创建一个新的数组,使用每个表达式的主体(用一致的参数替换参数(作为数组中的每个项。

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来写出用于将MyClass选择为ExtendedMyClass的lambda,同时使用之前生成的选择器来选择额外的字段:

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表达式,编译器会对其进行类型安全性验证。

我认为以extraFields为例,想象一下你需要的表达式是什么样子,然后找出如何实际创建它是很有帮助的。

所以,如果你有:

var extraFields = new Dictionary<string, Expression<Func<MyClass, bool>>>
{
    { "Foo", x => x.Foo },
    { "Bar", x => x.Bar }
};

然后你想生成这样的东西:

myQueryable.Select(
    x => new ExtendedMyClass
    {
        MyObject = x,
        ExtraFieldValues =
            new[]
            {
                new StringAndBool { FieldName = "Foo", IsTrue = x.Foo },
                new StringAndBool { FieldName = "Bar", IsTrue = x.Bar }
            }
    });

现在您可以使用表达式树API和LINQKit来创建这个表达式:

public static IQueryable<ExtendedMyClass> Extend(
    IQueryable<MyClass> myQueryable,
    Dictionary<string, Expression<Func<MyClass, bool>>> extraFields)
{
    Func<Expression<Func<MyClass, bool>>, MyClass, bool> invoke =
        LinqKit.Extensions.Invoke;
    var parameter = Expression.Parameter(typeof(MyClass));
    var extraFieldsExpression =
        Expression.Lambda<Func<MyClass, StringAndBool[]>>(
            Expression.NewArrayInit(
                typeof(StringAndBool),
                extraFields.Select(
                    field => Expression.MemberInit(
                        Expression.New(typeof(StringAndBool)),
                        new MemberBinding[]
                        {
                            Expression.Bind(
                                typeof(StringAndBool).GetProperty("FieldName"),
                                Expression.Constant(field.Key)),
                            Expression.Bind(
                                typeof(StringAndBool).GetProperty("IsTrue"),
                                Expression.Call(
                                    invoke.Method,
                                    Expression.Constant(field.Value),
                                    parameter))
                        }))),
            parameter);
    Expression<Func<MyClass, ExtendedMyClass>> selectExpression =
        x => new ExtendedMyClass
        {
            MyObject = x,
            ExtraFieldValues = extraFieldsExpression.Invoke(x)
        };
    return myQueryable.Select(selectExpression.Expand());
}