0

I have a LambdaExpression which has a lambda body like:

x => x.FirstName == "Joe" And x.LastName == "Bloggs"

It can also contain more properties and it can also contain OR operators.

How do I get a List<string> of all the property names used in that lambda?

So the list would look like below based on the lambda above:

FirstName
LastName 

There are other stackoverflow pages which somehow cast their Expression to MemberExpression, however this does not work for me because it won't cast properly.

An example of what I'm trying to do is like this answer below but that is returning PropertyInfo rather than a list of strings:

Retrieving Property name from lambda expression

chris
  • 148
  • 8
  • 4
    I think he wants to parse the expression and get list of all properties that were used in the expression - "FirstName" and "LastName" in this case. The question is clear. The problem here is that OP needs to give us more details - what the format of lambda is, is it only equal split by && operators? – MistyK Jun 23 '21 at 15:41
  • @MistyK Yup, you've got it right – chris Jun 23 '21 at 15:45
  • 1
    @MistyK: Apart from the fact that "parse" is the wrong verb, because the compiler's already parsed the source code and produced an expression tree object. – Ben Voigt Jun 23 '21 at 15:45
  • @chris: Lambda expressions do not exist by themselves. They are run on some data. The requests for you to provide more code means we need to see what data you are getting the data from. – Jonathan Wood Jun 23 '21 at 16:04
  • @MistyK: Lambda expressions are not used to "parse" expressions. – Jonathan Wood Jun 23 '21 at 16:04
  • @JonathanWood: The whole point of the `Expression` class is that lambda expressions are first-class objects that can be manipulated by themselves, without being black boxes that can only be invoked. – Ben Voigt Jun 23 '21 at 16:09
  • @BenVoigt: In order to know how they should be constructed to retrieve data from a set of data, we have to know what that set of data looks like. Yes, okay, you could say a lambda expression can exist by itself, but it's not useful by itself. The question needs more context. I'm not even convinced he's trying to create an expression. He might just be trying to retrieve certain data. – Jonathan Wood Jun 23 '21 at 16:11
  • 1
    @JonathanWood: No it really doesn't. OP isn't asking how to make a LambdaExpression, his question starts out already having one. – Ben Voigt Jun 23 '21 at 16:12

1 Answers1

3

.NET provides an ExpressionVisitor class that knows how to descend into subexpressions inside the tree. You can inherit from it and override VisitMember to get all MemberExpression instances contained in the tree, without caring whether they are combined via &&, ||, ==, <, function calls, etc.

Here is a complete working example:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;

namespace Rextester
{
    class MemberCollector : ExpressionVisitor
    {
        public HashSet<string> FoundMembers = new HashSet<string>();
        
        override protected Expression VisitMember (MemberExpression node)
        {
            FoundMembers.Add(node.Member.Name);
            return base.VisitMember(node);
        }
    }
    
    public class Program
    {
        public static void Main(string[] args)
        {
            var collector = new MemberCollector();
            Expression<Func<DateTime, bool>> e = dt => dt.Hour == 12 && dt.Minute % 10 == 0;
            collector.Visit(e);
            foreach (var m in collector.FoundMembers)
                Console.WriteLine(m);
        }
    }
}

I haven't filtered the member accesses so I will find field and method access in addition to properties, but that would be a straightforward test on node.Member.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
  • Nice idea/answer. I would suggest a slight API change, to make the MemberCollector static and use a nested class, to overcome the possible issue of visitng an expression twice, and maybe unexpected behaviour. like this: https://dotnetfiddle.net/IFbGoB – Rand Random Jun 23 '21 at 16:03
  • @RandRandom: Seems pointless. I expect anyone working with expression trees to know enough about object identity to know how many `new MemberCollector()` instances they want. You take away the flexibility of intentionally combining multiple results into a single collection. – Ben Voigt Jun 23 '21 at 16:07
  • Maybe its just me, but I wouldn't expect calling `.Visit()` multiple times to get a combined result. You can achieve the same with my suggest API but you have to do it explicitly eg. `var combined = MemberCollector.GetMembers(e).UnionWith(MemberCollector.GetMembers(e));` – Rand Random Jun 23 '21 at 16:14
  • @RandRandom: I don't think people are surprised by `List.AddRange` giving a combined result if you call it on the same List instance and separate results if you call on separate List instances. `FoundMembers` is a field of `MemberCollector` so of course its lifetime goes with the lifetime of the `MemberCollector` instance. – Ben Voigt Jun 23 '21 at 16:16
  • For example `DataTable` and `.Load`, I wouldn't expect (and I don't think it happens) calling `.Load` multiple times for `.Rows` to have a combined result of everytime `.Load` got called, I would have expected for `.Load` to reset `.Rows`. – Rand Random Jun 23 '21 at 16:22
  • 1
    @RandRandom: First paragraph in [the documentation](https://learn.microsoft.com/en-us/dotnet/api/system.data.datatable.load?view=net-5.0)... "If the DataTable already contains rows, the incoming data from the data source is merged with the existing rows". It's so easy to call `Clear()` if you don't want accumulation, but if `Load()` did clear automatically, getting accumulation would be very very inefficient. Think about how the "Single Responsibility Principle" applies here. – Ben Voigt Jun 23 '21 at 16:36
  • @RandRandom: Note that I have no complaint about providing a convenience wrapper of the sort you described... as long as the simpler, SRP-compliant object is also available so one can choose not to use the wrapper. What I didn't like about your proposal was that you hid the building blocks inside the wrapper, forcing the user to pay for a new separate instance even if they didn't want one. – Ben Voigt Jun 23 '21 at 16:40
  • Am surprised to see the behavior of Load, always thought about it as Load a new set of data, and applied this mind set to your proposed api where visiting an expression was for me a new set of data. I admit defeat. ;) – Rand Random Jun 23 '21 at 16:50