Rewriting Linq Select to a new subpath

108 views Asked by At

I am currently trying to dynamically build linq queries. I want to be able to reuse Linq expressions in other Linq expressions. For example:

    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; }
    }

When writing the converter for the Dummy class, the converter should be able to reuse the Sub.Converter. For thi spurpose I have written a DynamicSelect<> extension method:

var result = Query
                .Where(x=>x.Sub != null)
                .DynamicSelect(x => new Result())
                    .Select(x => x.SubText, x => x.Sub,Sub.Converter)
                .Apply()
            .ToList();

DynamicSelect creates a new SelectionBuilder, the select part takes as first input the targetproperty (Result.SubText), as second property the input property which we want to convert and as third input the converter for the Sub Property. The .Apply call then returns the builtup expression tree.

I managed to get it working for a simpler usecase (without the subpath):

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();

But how do I rebase an expression to another subpath?

Code so far:

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);
    }
}
1

There are 1 answers

0
Peter Schojer On BEST ANSWER

Found the solution. I needed to write a new Expression visitor that added the extra member acces(es):

    /// <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;
        }
    }

The single property Select method is then rather trivial:

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