使用表达式访问嵌套的属性和集合

问题描述

我有以下课程:

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

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

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

    [JsonProperty("profession")]
    public Profession Profession { get; set; }

    [JsonProperty("hobbies")]
    public List<string> Hobbies { get; set; }
}

public class Profession
{
    [JsonProperty("name")]
    public string ProfessionName { get; set; }

    [JsonProperty("industry")]
    public string Industry { get; set; }

    [JsonProperty("salary")]
    public string AverageSalary { get; set; }

    [JsonProperty("activities")]
    public List<WorkActivity> WorkActivities { get; set; }
}

public class WorkActivity
{
    [JsonProperty("id")]
    public int Id { get; set; }

    [JsonProperty("rooms")]
    public List<string> Rooms { get; set; }
}

public class PropertiesVisitor : ExpressionVisitor
{
    private readonly Expression param;

    public List<string> Names { get; } = new List<string>();

    public PropertiesVisitor(Expression parameter)
    {
        param = parameter;
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        if (node.Expression == param)
        {
            Names.Add(node.Member.GetCustomAttribute<JsonPropertyAttribute>().PropertyName);
        }

        return base.VisitMember(node);
    }
}

我像这样调用我的应用程序中的方法

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

public static void Main(string[] args)
{
    var names = FindPropertyNames<Person>(x => new { x.Age,x.Country,x.Profession.AverageSalary,x.Hobbies });
    Console.WriteLine(string.Join(" ",names));
}

它工作正常,除了一个小细节 - 它不检查嵌套属性输出如下:age country profession hobbies。我希望它是 age country profession.salary hobbies

我一直在尝试使用不同的方法解决该问题,但无法完全解决。我尝试了以下方法

public static MemberExpression GetMemberExpression(Expression e)
{
    if (e is MemberExpression)
    {
        return (MemberExpression)e;
    }
    else if (e is LambdaExpression)
    {
        var le = e as LambdaExpression;
        if (le.Body is MemberExpression)
        {
            return (MemberExpression)le.Body;
        }
        else if (le.Body is UnaryExpression)
        {
            return (MemberExpression)((UnaryExpression)le.Body).Operand;
        }
    }
    return null;
}

public static string GetPropertyPath<T>(Expression<Func<T,object>> expr)
{
    var path = new StringBuilder();
    MemberExpression me = GetMemberExpression(expr);

    do
    {
        if (path.Length > 0)
        {
            path.Insert(0,".");
        }
        path.Insert(0,me.Member.GetCustomAttribute<JsonPropertyAttribute>().PropertyName);
        me = GetMemberExpression(me.Expression);
    }
    while (me != null);
    return path.ToString();
}

它有点作用 - 但它不能超过一个属性

我这样称呼它:

var x = GetPropertyPath<Person>(p => p.Profession.AverageSalary);
Console.WriteLine(x);

我希望能够发送多个属性,就像在第一个版本中一样。另外,我不确定如何将以下 person.Profession.WorkActivities.Rooms 作为参数传递,因为它是一个列表。我想得到 profession.activities.rooms 作为它的输出

解决方法

我将编写一个接受 Expression 并返回链中所有成员的方法:

public static IEnumerable<MemberExpression> MemberClauses(this Expression expr) {
    if (expr is not MemberExpression mexpr) {
        yield break;
    }
    foreach (var item in MemberClauses(mexpr.Expression)) {
        yield return item;
    }
    yield return mexpr;
}

然后,您可以解开整个方法链和 LINQ 以获取 JSON 属性名称:

public class PropertiesVisitor : ExpressionVisitor {
    private readonly Expression param;

    public List<string> Names { get; } = new List<string>();

    public PropertiesVisitor(Expression parameter) => param = parameter;

    [return: NotNullIfNotNull("node")]
    public override Expression Visit(Expression node) {
        var chain = node.MemberClauses().ToList();
        if (chain.Any() && chain.First().Expression == param) {
            var name = string.Join(".",chain.Select(
                mexpr => mexpr.Member.GetCustomAttribute<JsonPropertyAttribute>().PropertyName
            ));
            Names.Add(name);
            return node;
        } else {
            return base.Visit(node);
        }
    }
}

然后你可以得到如下的属性路径:

public static class Functions {
    public static List<string> GetPropertyPath<T>(Expression<Func<Person,T>> expr) {
        var visitor = new PropertiesVisitor(expr.Parameters[0]);
        visitor.Visit(expr);
        return visitor.Names;
    }
}

var names = Functions.GetPropertyPath(p => new { p.Age,p.Country,p.Profession.AverageSalary,p.Hobbies });
foreach (var name in names) {
    Console.WriteLine(name);
}