Sha*_*ica 4 c# linq linq-to-entities dynamic-linq linqkit
假设我已经定义了以下变量:
IQueryable<MyClass> myQueryable;
Dictionary<string, Expression<Func<MyClass, bool>>> extraFields;
// the dictionary is keyed by a field name
Run Code Online (Sandbox Code Playgroud)
现在,我想将一些动态字段添加到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;}
}
Run Code Online (Sandbox Code Playgroud)
换句话说,对于每个值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);
}
Run Code Online (Sandbox Code Playgroud)
为此,我们只需将第二个表达式主体中第二个参数的所有实例替换为第一个表达式的主体.我们还需要确保两个实现对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);
}
Run Code Online (Sandbox Code Playgroud)
接下来,我们将编写一个接受一系列表达式的方法,这些表达式接受相同的输入并计算相同类型的输出.它会将其转换为接受相同输入的单个表达式,但作为结果计算输出序列,其中序列中的每个项目表示每个输入表达式的结果.
这种实现相当简单; 我们创建一个新数组,使用每个表达式的主体(用一致的替换参数)作为数组中的每个项目.
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);
}
Run Code Online (Sandbox Code Playgroud)
现在我们已经完成了所有这些通用帮助方法,我们可以开始处理您的具体情况.
这里的第一步是将你的字典变成一系列表达式,每个表达式接受一个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
}));
Run Code Online (Sandbox Code Playgroud)
现在我们可以使用我们的AsSequence方法将它从一系列选择器转换为选择出序列的单个选择器:
Expression<Func<MyClass, IEnumerable<StringAndBool>>> extrafieldsSelector =
stringAndBools.AsSequence();
Run Code Online (Sandbox Code Playgroud)
现在我们差不多完成了.现在,我们只需要使用Combine这个表达式写出来我们的拉姆达用于选择MyClass到ExtendedMyClass使用选择出额外的字段先前生成的选择,同时:
var finalQuery = myQueryable.Select(
extrafieldsSelector.Combine((foo, extraFieldValues) =>
new ExtendedMyClass
{
MyObject = foo,
ExtraFieldValues = extraFieldValues,
}));
Run Code Online (Sandbox Code Playgroud)
我们可以使用相同的代码,删除中间变量并依赖类型推断将其下拉到单个语句,假设您没有发现它太过于不合情理:
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,
}));
Run Code Online (Sandbox Code Playgroud)
值得注意的是,这种通用方法的一个关键优势是使用更高级别的Expression方法会产生至少可以合理理解的代码,而且在编译时可以静态验证是类型安全的.这里有一些通用的,可重用的,可测试的,可验证的扩展方法,一旦编写,允许我们纯粹通过方法和lambda的组合来解决问题,并且不需要任何实际的表达式操作,这两者都是复杂,容易出错,并消除所有类型的安全.这些扩展方法中的每一个都是这样设计的,只要输入表达式有效,结果表达式总是有效的,并且这里的输入表达式都是有效的,因为它们是lambda表达式,编译器验证它们用于类型安全.