重写Linq Select到新的子路径
本文关键字:路径 Linq Select 重写 | 更新日期: 2023-09-27 17:59:44
我目前正在尝试动态构建linq查询。我希望能够在其他Linq表达式中重用Linq表达式。例如:
public class Dummy
{
public string Test { get; set; }
public Sub Sub { get; set; }
}
public class Sub
{
public static Expression<Func<Sub, string>> Converter
{
get
{
return x => x.Id + ": " + x.Text;
}
}
public int Id { get; set; }
public string Text { get; set; }
}
在为Dummy类编写转换器时,转换器应该能够重用Sub.converter。为此,我编写了一个DynamicSelect<>扩展方法:
var result = Query
.Where(x=>x.Sub != null)
.DynamicSelect(x => new Result())
.Select(x => x.SubText, x => x.Sub,Sub.Converter)
.Apply()
.ToList();
DynamicSelect创建一个新的SelectionBuilder,选择部分将targetproperty(Result.SubText)作为第一输入,第二个属性是我们想要转换的输入属性,第三个输入是Sub属性的转换器。.Apply调用然后返回内置表达式树。
我设法使它适用于一个更简单的用例(没有子路径):
var result = Query.DynamicSelect(x => new Result())
.Select(x => x.ResolvedTest, x => x.Inner == null ? x.Test : x.Inner.Test)
.Select(x => x.SubText, x => x.Sub == null ? null : x.Sub.Text)
.Apply()
.ToList();
但是,如何将一个表达式重新设置为另一个子路径呢?
到目前为止的代码:
public static class SelectBuilder
{
public static LinqDynamicConverter<TInput,TOutput> DynamicSelect<TInput,TOutput>(
this IQueryable<TInput> data,
Expression<Func<TInput, TOutput>> initialConverter) where TOutput : new()
{
return new LinqDynamicConverter<TInput,TOutput>(data, initialConverter);
}
}
public class LinqDynamicConverter<TInput,TOutput> where TOutput: new()
{
#region inner classes
private class MemberAssignmentVisitor : ExpressionVisitor
{
private IDictionary<MemberInfo, MemberAssignment> SinglePropertyToBinding { get; set; }
public MemberAssignmentVisitor(IDictionary<MemberInfo, MemberAssignment> singlePropertyToBinding)
{
SinglePropertyToBinding = singlePropertyToBinding;
}
protected override MemberAssignment VisitMemberAssignment(MemberAssignment node)
{
SinglePropertyToBinding[node.Member] = node;
return base.VisitMemberAssignment(node);
}
}
private class MemberInfoVisitor : ExpressionVisitor
{
internal MemberInfo SingleProperty { get; private set; }
public MemberInfoVisitor()
{
}
protected override Expression VisitMember(MemberExpression node)
{
SingleProperty = node.Member;
return base.VisitMember(node);
}
}
#endregion
#region properties
private IQueryable<TInput> Data { get;set; }
private Expression<Func<TInput, TOutput>> InitialConverter { get;set;}
private IDictionary<MemberInfo, MemberAssignment> SinglePropertyToBinding { get; set; }
#endregion
#region constructor
internal LinqDynamicConverter(IQueryable<TInput> data,
Expression<Func<TInput, TOutput>> initialConverter)
{
Data = data;
InitialConverter = x => new TOutput(); // start with a clean plate
var replace = initialConverter.Replace(initialConverter.Parameters[0], InitialConverter.Parameters[0]);
SinglePropertyToBinding = new Dictionary<MemberInfo, MemberAssignment>();
MemberAssignmentVisitor v = new MemberAssignmentVisitor(SinglePropertyToBinding);
v.Visit(initialConverter);
}
#endregion
public LinqDynamicConverter<TInput,TOutput> Select<TProperty,TConverted>(
Expression<Func<TOutput, TConverted>> initializedOutputProperty,
Expression<Func<TInput, TProperty>> subPath,
Expression<Func<TProperty, TConverted>> subSelect)
{
//????
return this;
}
// this one works
public LinqDynamicConverter<TInput,TOutput> Select<TProperty>(
Expression<Func<TOutput, TProperty>> initializedOutputProperty,
Expression<Func<TInput, TProperty>> subSelect)
{
var miv = new MemberInfoVisitor();
miv.Visit(initializedOutputProperty);
var mi = miv.SingleProperty;
var param = InitialConverter.Parameters[0];
Expression<Func<TInput, TProperty>> replace = (Expression<Func<TInput, TProperty>>)subSelect.Replace(subSelect.Parameters[0], param);
var bind = Expression.Bind(mi, replace.Body);
SinglePropertyToBinding[mi] = bind;
return this;
}
public IQueryable<TOutput> Apply()
{
var converter = Expression.Lambda<Func<TInput, TOutput>>(
Expression.MemberInit((NewExpression)InitialConverter.Body, SinglePropertyToBinding.Values), InitialConverter.Parameters[0]);
return Data.Select(converter);
}
}
找到了解决方案。我需要写一个新的Expression访问者,添加额外的成员访问:
/// <summary>
/// rebinds a full expression tree to a new single property
/// Example: we get x => x.Sub as subPath. Now the visitor starts visiting a new
/// expression x => x.Text. The result should be x => x.Sub.Text.
/// Traversing member accesses always starts with the rightmost side working toward the parameterexpression.
/// So when we reach the parameterexpression we have to inject the whole subpath including the parameter of the subpath
/// </summary>
/// <typeparam name="TConverted"></typeparam>
/// <typeparam name="TProperty"></typeparam>
private class LinqRebindVisitor<TConverted, TProperty> : ExpressionVisitor
{
public Expression<Func<TInput, TConverted>> ResultExpression { get; private set; }
private ParameterExpression SubPathParameter { get; set; }
private Expression SubPathBody { get; set; }
private bool InitialMode { get; set; }
public LinqRebindVisitor(Expression<Func<TInput, TProperty>> subPath)
{
SubPathParameter = subPath.Parameters[0];
SubPathBody = subPath.Body;
InitialMode = true;
}
protected override Expression VisitMember(MemberExpression node)
{
// Note that we cannot overwrite visitparameter because that method must return a parameterexpression
// So whenever we detect that our next expression will be a parameterexpression, we inject the subtree
if (node.Expression is ParameterExpression && node.Expression != SubPathParameter)
{
var expr = Visit(SubPathBody);
return Expression.MakeMemberAccess(expr, node.Member);
}
return base.VisitMember(node);
}
protected override Expression VisitLambda<T>(Expression<T> node)
{
bool initialMode = InitialMode;
InitialMode = false;
Expression<T> expr = (Expression<T>)base.VisitLambda<T>(node);
if (initialMode)
{
ResultExpression = Expression.Lambda<Func<TInput, TConverted>>(expr.Body,SubPathParameter);
}
return expr;
}
}
单一属性Select方法则相当琐碎:
public LinqDynamicConverter<TInput,TOutput> Select<TProperty,TConverted>(
Expression<Func<TOutput, TConverted>> initializedOutputProperty,
Expression<Func<TInput, TProperty>> subPath,
Expression<Func<TProperty, TConverted>> subSelect)
{
LinqRebindVisitor<TConverted, TProperty> rebindVisitor = new LinqRebindVisitor<TConverted, TProperty>(subPath);
rebindVisitor.Visit(subSelect);
var result = rebindVisitor.ResultExpression;
return Property<TConverted>(initializedOutputProperty, result);
}