Axajkamkat
Axajkamkat

Reputation: 179

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.

Upvotes: 0

Views: 270

Answers (1)

canton7
canton7

Reputation: 42235

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.

Upvotes: 2

Related Questions