Search code examples
c#lambdareflectionexpressionexpression-trees

Create a list of strongly typed variable names using Expression


I have a class:

public class Student
{
    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("age")]
    public int Age { get; set; }

    [JsonProperty("country")]
    public string Country { get; set; }
}

And I have a method:

public static List<string> PrintPropertyNames<T>(params Expression<Func<T, object>>[] properties)
{
    var list = new List<string>();

    foreach (var p in properties)
    {
        if (p.Body is MemberExpression)
        {
            var e = (MemberExpression)p.Body;
            list.Add(((JsonPropertyAttribute)e.Member.GetCustomAttribute(typeof(JsonPropertyAttribute))).PropertyName);
        }
        else
        {
            var e = (MemberExpression)((UnaryExpression)p.Body).Operand;
            list.Add(((JsonPropertyAttribute)e.Member.GetCustomAttribute(typeof(JsonPropertyAttribute))).PropertyName);
        }
    }

    return list;
}

Which I call like this:

Console.WriteLine(string.Join(" ", PrintPropertyNames<Student>(x => x.Age, x => x.Country)));

Now, I want to modify my method definition to take only one parameter, but I can't figure out how to do it.

I tried doing something like this:

public static List<string> PrintPropertyNames2<T>(Expression<Func<T, object>>[] properties)

Which I call like this:

Console.WriteLine(string.Join(" ", PrintPropertyNames2<Student>(new Expression<Func<Student, object>>[] { x => x.Age, x => x.Country })));

I tried simplifying it to:

Console.WriteLine(string.Join(" ", PrintPropertyNames2<Student>(new [] { x => x.Age, x => x.Country })));

But the compiler couldn't find the best suitable type. So I have to explicitly write the type, which looks ugly and not what I really want anyways. I need it generic.

What I want to do in the final version is the following:

Console.WriteLine(string.Join(" ", PrintPropertyNames<Student>(x => x.Age && x.Country && x.Name))); (output should be - age country name)

I'm not sure if that's possible, but I want to put all my properties inside a single expression and obtain their json attribute value at once.


Solution

  • For starters, you can't use x => x.Age && x.Country && x.Name -- Age is an int and Name is a string, and you can't combine those with &&, so you'll get a compiler error. But we can use + instead as string concatentation, or return a new { x.Name, x.Age, x.Country } or new object[] { x.Name, x.Age, x.Country }.

    Either way, the easiest way to do this is to use an ExpressionVisitor to find all MemberAccess expressions which access a property on our input Student, wherever they're buried in the expression:

    public class FindPropertiesVisitor : ExpressionVisitor
    {
        private readonly Expression parameter;
        public List<string> Names { get; } = new List<string>();
    
        public FindPropertiesVisitor(Expression parameter) => this.parameter = parameter;
    
        protected override Expression VisitMember(MemberExpression node)
        {
            if (node.Expression == parameter)
            {
                Names.Add(node.Member.GetCustomAttribute<JsonPropertyAttribute>().PropertyName);
            }
            return node;
        }
    }
    

    Using this is pretty straightforward:

    public static List<string> FindPropertyNames<T>(Expression<Func<T, object>> expr)
    {
        var visitor = new FindPropertiesVisitor(expr.Parameters[0]);
        visitor.Visit(expr);
        return visitor.Names;
    }
    

    We pass in expr.Parameters[0] as the Student expression, which we want to find member accesses on.

    You can then call it with any expression which accesses those properties in any way, e.g.:

    var names = FindPropertyNames<Student>(x => new { x.Name, x.Age, x.Country });
    var names = FindPropertyNames<Student>(x => new object[] { x.Name, x.Age, x.Country });
    var names = FindPropertyNames<Student>(x => x.Name + x.Age + x.Country);
    

    See it working here.