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

假设我已经定义了以下变量:

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

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

 class ExtendedMyClass { public MyClass MyObject {get; set;} public IEnumerable ExtraFieldValues {get; set;} } class StringAndBool { public string FieldName {get; set;} public bool IsTrue {get; set;} } 

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

我有一种感觉,这应该在动态的Linq和LinqKit中可行,尽管我之前从未认真对待过。 我也对其他建议持开放态度,特别是如果这可以通过良好的“powershell型Linq”来完成。

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

因此,我们在这里会有很多步骤,但每个步骤应该相当简短,自包含, 可重用且相对容易理解。

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

 public static Expression> Combine( this Expression> first, Expression> 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>(newSecond, param); } 

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

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

 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>> AsSequence( this IEnumerable>> 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>>(body, param); } 

现在我们已经完成了所有这些通用帮助方法,我们可以开始处理您的具体情况。

这里的第一步是将您的字典转换为表达式序列,每个表达式接受一个MyClass并创建一个表示该对的StringAndBool 。 为此,我们将对字典的值使用Combine ,然后使用lambda作为第二个表达式,使用它的中间结果来计算StringAndBool对象,此外还要关闭该对的键。

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

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

 Expression>> 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方法会产生至少可以合理理解的代码,但也可以在编译时静态validation类型安全 。 这里有一些通用的,可重用的,可测试的,可validation的扩展方法,一旦编写,允许我们纯粹通过方法和lambda的组合来解决问题,并且不需要任何实际的表达式操作,这两者都是复杂,容易出错,并消除所有类型的安全。 这些扩展方法中的每一个都是这样设计的,只要输入表达式有效,结果表达式总是有效的,并且这里的输入表达式都是有效的,因为它们是lambda表达式,编译器validation它们为了类型安全。

我认为在这里采用一个示例extraFields是有帮助的,想象一下你需要的表达式是什么样的,然后弄清楚如何实际创建它。

所以,如果你有:

 var extraFields = new Dictionary>> { { "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 Extend( IQueryable myQueryable, Dictionary>> extraFields) { Func>, MyClass, bool> invoke = LinqKit.Extensions.Invoke; var parameter = Expression.Parameter(typeof(MyClass)); var extraFieldsExpression = Expression.Lambda>( 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> selectExpression = x => new ExtendedMyClass { MyObject = x, ExtraFieldValues = extraFieldsExpression.Invoke(x) }; return myQueryable.Select(selectExpression.Expand()); }