具有集合项属性的 C# 表达式 - 访问成员名称

C# Expression with collection item properties - access member names

我正在构建一个 SDK,用于构建对特定系统的 HTTP 查询,我需要在查询字符串中指定我想要包含的模型属性。

例如https://system/api/projects/1?fields=name,description

我希望 SDK 是强类型的,所以我有查询生成器 类,它允许将查询指定为

new ProjectBuilder(1, f => f.Name, f => f.Description)

即使对于嵌套对象的复杂树,它也能很好地工作,例如f => f.ProjectTemplate.Location.Owner.Email

唯一的问题是集合,例如

public class Task
{
   public string Name {get;set;}
  //lots of other stuff
}

public string Project
{
   public string Description {get;set;}
  //lots of other stuff
   public List<Task> Tasks {get;set;}
}

当我需要检索 ProjectDescriptionProject 中所有 Tasks 的名称时,查询字符串必须如下所示:

https://system/api/projects/1?fields=description,tasks.name

我不能定义这样的表达式:

new ProjectBuilder(1, f => f.Tasks.Name),语法好像需要f.Tasks[0].Name.

我可以对集合成员(以及进一步的嵌套对象)使用相同的表达式类型语法吗?

我用于从表达式访问成员的代码如下(稍微简化):

        public static string Evaluate<T>(Expression<Func<T, object>> expression)
        {
            if (expression.Body is MemberExpression body)
            {
                return EvaluateExpressionTree(body);
            }
            else
            {
                throw new InvalidOperationException(
                    "Invalid expression. Expected Member expression, e.g. p=>p.Description");
            }
        }
            private static string EvaluateExpressionTree(MemberExpression root)
            {
                if (root.Expression is MemberExpression nested)
                {
                    var nestedProperty = EvaluateExpressionTree(nested);
                    var thisProperty = root.Member.Name;
                    return nestedProperty + "." + thisProperty;
                }
                else if (root.Expression is MethodCallExpression call)
                {
                    //that's where I get when using the Tasks[0] syntax
                 }
                else
                {
                    return GetMemberName(root);
                }
            }
    
          

当表达式访问集合元素时,我能够从集合中找到泛型类型,但从那时起我无法重建表达式的其他元素...

所以我设法解决了这个问题。并不是说这是处理具有集合属性的表达式的方式,但问题是具体的,答案也是如此:)

概述

解决方案是

  • 字符串化表达式
  • 标记它
  • 去除噪音
  • 通过反射获取 PropertyInfo 来评估部件
  • 跟踪表达式树中的当前类型以获得正确的结果

这使我能够非常流畅地 API 生成查询字符串的强类型,如下所示。

代码

用法

   [TestMethod]
    public void ProjectExpression_NestedType_CollectionAccess()
    {
        //arrange
        ProjectBuilder builder = new ProjectBuilder("000",
                f => f.Name,
                f => f.Workflow.TaskConfigurations,
                f => f.Workflow.TaskConfigurations[0].TaskTemplate.TaskType)
            ;

        //act
        string stringified = builder.BuildFullRequestUri();

        //assert
        Assert.AreEqual("projects/000?fields=name,workflow.taskConfigurations,workflow.taskConfigurations.taskTemplate.taskType", stringified);
    }

实施:

接受函数参数的调用者代码:

 protected ProjectBuilder(params Expression<Func<Project, object>>[] propertiesToInclude)
    {
        List<string> values = new List<string>();
        foreach (Expression<Func<Project, object>> expression in propertiesToInclude)
        {
            values.Add(ExpressionEvaluator.Evaluate(expression));
        }
        this.AddProperties(values);
    }

以及接受每个参数并很好地转换它的表达式求值器。

internal static class ExpressionEvaluator
{
    public static string Evaluate<T>(Expression<Func<T, object>> expression)
    {
        if (expression.Body is MemberExpression)
        {
            List<string> result = new List<string>();
            List<string> parts = GetExpressionParts(expression);

            List<Type> expressionTreeTypes = new List<Type>() { typeof(T)  };
            foreach (string part in parts) 
            {
                PropertyInfo member = expressionTreeTypes.Last().GetProperty(part);
                if (member != null)
                {
                    AddValueFromJsonAttributeOrPropertyName<T>(member, result);
                    UpdateLastUsedType<T>(member, expressionTreeTypes);
                }
                else
                {
                    throw new InvalidOperationException($"Expression [{expression.Body}] is not valid. Failed to resolve: [{part}]");
                }
            }
            return string.Join(".", result);
        }
        else
        {
            throw new InvalidOperationException("Invalid expression. Expected Member expression, e.g. p=>p.Description");
        }
    }

    private static List<string> GetExpressionParts<T>(Expression<Func<T, object>> expression)
    {
        var stringified = expression.Body.ToString();
        //skip the first part of expression - e.g. (f=>f.) - skip 'f.
        //also skip collection accessors
        return stringified.Split('.').Where(x => !x.Contains("get_Item(")).Skip(1).ToList();
    }

    private static void UpdateLastUsedType<T>(PropertyInfo member, List<Type> expressionTreeTypes)
    {
        //make sure that in case of primitive types we don't change the type
        if (member.PropertyType.Assembly == typeof(Project).Assembly && member.PropertyType != expressionTreeTypes.Last())
        {
            expressionTreeTypes.Add(member.PropertyType);
        }
        else if (member.PropertyType.IsGenericType) // in case of collection properties, extract the nested type 
        {
            expressionTreeTypes.Add(member.PropertyType.GenericTypeArguments.First());
        }
    }

    private static void AddValueFromJsonAttributeOrPropertyName<T>(MemberInfo member, List<string> result)
    {
        var attr = member.CustomAttributes
            .FirstOrDefault(x => x.AttributeType == typeof(JsonPropertyAttribute))?.ConstructorArguments?
            .FirstOrDefault();
        if (!string.IsNullOrEmpty(attr?.Value?.ToString()))
        {
            result.Add(attr?.Value?.ToString());
        }
        else
        {
            result.Add(member.Name[0].ToString().ToLower() + member.Name.Substring(1));
        }
    }
}