所以,我们在这里会有很多步骤,但每个单独的步骤都应该相当短、独立、可重用且相对容易理解。
我们要做的第一件事是创建一个可以组合表达式的方法。它将做一个接受一些输入并生成中间值的表达式。然后它将接受第二个表达式作为输入,接受与第一个相同的输入,中间结果的类型,然后计算一个新的结果。它将返回一个新的表达式,接受第一个的输入,并返回第二个的输出。
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);
}
现在我们已经掌握了所有这些通用帮助方法,我们可以开始处理您的具体情况。
这里的第一步是将你的字典变成一系列表达式,每个都接受 aMyClass
并创建一个StringAndBool
表示该对的 a 。为此,我们将使用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 来选择 a 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 表达式为了类型安全。