4
\$\begingroup\$

I need to extract all member accesses to expression parameter (and make sure, that they are not nested). For instance, for expression: a => a.A + a.B I need to receive list {"A", "B"}. For expression a => a.B.C exception should be thrown, as we have nested member access.

I wrote the following class, but I'm not too satisfied with it. Mainly because I don't like pattern "Call, then access member for result", because it decreases code maintainability and allows introducing hard to find bugs (ie. what if I accidentally access results later without calling etc.)

I'm wondering, if this class might be implemented in such way, so that I could call:

List<string> members = new MemberExpressionVisitor<A, bool>().GetMembers(a => a.A + a.B);

My current solution follows:

private class MemberExpressionVisitor<T1, T2> : ExpressionVisitor
{
    private Expression<Func<T1, T2>> originalExpression = null;
    private HashSet<string> accessedMembers = null;

    public override Expression Visit(Expression node)
    {
        if (originalExpression == null)
        {
            if (node is Expression<Func<T1, T2>> startExpression)
            {
                originalExpression = startExpression;
                accessedMembers = new HashSet<string>();

                var result = base.Visit(node);

                originalExpression = null;

                return result;
            }
            else
            {
                throw new InvalidOperationException($"Passed expression must be of type Expression<Func<{typeof(T1).Name},{typeof(T2).Name}>>!");
            }
        }
        else
        {
            return base.Visit(node);
        }
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        if (node.Expression is MemberExpression)
            throw new InvalidOperationException("Nested member accesses are not allowed!");

        if (node.Expression is ParameterExpression parameterExpression)
        {
            if (parameterExpression.Name == originalExpression.Parameters[0].Name)
                accessedMembers.Add(node.Member.Name);
        }

        return base.VisitMember(node);
    }

    public List<string> GetAccessedMembers() => accessedMembers.ToList();           
}

Example call:

public class B
{
    public int P => 5;
}

public class A
{
    public B B => new B();
    public int C => 8;
}

static void Main(string[] args)
{
    Expression<Func<A, bool>> exp = a => a.B != null && a.C ==8;

    MemberExpressionVisitor<A, bool> visitor = new MemberExpressionVisitor<A, bool>();
    visitor.Visit(exp);

    Console.WriteLine(String.Join(", ", visitor.GetAccessedMembers()));
    Console.ReadKey();
}

Edit: Safer, but with clumsy "processing" field.

private class MemberExpressionExtractor : ExpressionVisitor
{
    private bool processing = false;
    private string parameterName;
    private readonly HashSet<string> accessedMembers = new HashSet<string>();

    public List<string> ExtractMembers<T1, T2>(Expression<Func<T1, T2>> expression)
    {
        try
        {
            processing = true;

            parameterName = expression.Parameters[0].Name;
            accessedMembers.Clear();

            Visit(expression);
        }
        finally
        {
            processing = false;
        }

        return accessedMembers.ToList();
    }

    public override Expression Visit(Expression node)
    {
        if (!processing)
            throw new InvalidOperationException("Use ExtractMembers<T1, T2> instead.");

        return base.Visit(node);
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        if (node.Expression is MemberExpression)
            throw new InvalidOperationException("Nested member accesses are not allowed!");

        if (node.Expression is ParameterExpression parameterExpression)
        {
            if (parameterExpression.Name == parameterName)
                accessedMembers.Add(node.Member.Name);
        }

        return base.VisitMember(node);
    }
}
```
\$\endgroup\$
1
  • \$\begingroup\$ Do you really need to only collect member accesses off the original lambda's first parameter? \$\endgroup\$
    – NetMage
    Commented Oct 7, 2020 at 22:00

1 Answer 1

1
\$\begingroup\$

What if you nested the MemberExpressionVisitor class inside another class? Since I like extension methods, create a static class that contains an extension method on Expression<Func<T,TRes>> that creates the visitor, runs it and returns the result:

static class LambdaExt {
    public static List<string> GetAccessedMemberNames<T, TRes>(this Expression<Func<T, TRes>> startExpr) {
        var v = new MemberExpressionVisitor<T, TRes>();
        v.Visit(startExpr);

        return v.GetAccessedMembers();
    }

    // put MemberExpressionVisitor definition here
}

Now you can use it like:

Console.WriteLine(String.Join(", ", exp.GetAccessedMemberNames()));

Alternatively, you can add a constructor to MemberExpressionVisitor:

public MemberExpressionVisitor(Expression<Func<T1, T2>> startExpression) {
    originalExpression = startExpression;
    Visit(startExpression);
}

remove the Visit override entirely, and simplify the extension method to:

public static List<string> GetAccessedMemberNames<T, TRes>(this Expression<Func<T, TRes>> startExpr) =>
    new MemberExpressionVisitor<T,TRes>(startExpr).GetAccessedMembers();

This has the compiler enforce type safety, assuming you know your lambda expression variables types properly.

\$\endgroup\$
2
  • \$\begingroup\$ Once you're hiding the MemberExpressionVisitor, is there any gain in the type parameters of MemberExpressionVisitor? \$\endgroup\$
    – Zev Spitz
    Commented Jul 6, 2021 at 8:01
  • 1
    \$\begingroup\$ @ZevSpitz I think you should be able to replace Expression<Func<T1,T2>> with LambdaExpression and it should all work. \$\endgroup\$
    – NetMage
    Commented Jul 6, 2021 at 20:38

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.