重写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);
    }
}

重写Linq Select到新的子路径

找到了解决方案。我需要写一个新的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);
    }