【问题标题】:Creating a dynamic Linq select clause from Expressions从表达式创建动态 Linq 选择子句
【发布时间】:2014-06-16 20:20:52
【问题描述】:

假设我定义了以下变量:

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

现在,我想在 IQueryable 中添加一些动态字段,以便它返回一个 IQueryable&lt;ExtendedMyClass&gt;,其中 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 中有一个值,表示该表达式对该行的计算结果是否为真。

我觉得这在动态 Linq 和 LinqKit 中应该是可行的,尽管我以前从未认真使用过。我也愿意接受其他建议,特别是如果这可以在良好的强类型 Linq 中完成。

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

【问题讨论】:

    标签: c# linq linq-to-entities dynamic-linq linqkit


    【解决方案1】:

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

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

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

    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 表达式为了类型安全。

    【讨论】:

    • 谢谢!真正漂亮的解决方案!
    【解决方案2】:

    我认为这里以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());
    }
    

    【讨论】:

    • 请注意,我实际上并没有用 EF 对此进行过测试,但我看不出它不应该工作的任何原因。
    猜你喜欢
    • 2011-03-16
    • 2015-02-22
    • 2016-12-25
    • 1970-01-01
    • 2011-10-03
    • 1970-01-01
    • 2014-07-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多