具有集合项属性的 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;}
}
当我需要检索 Project
的 Description
和 Project
中所有 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));
}
}
}
我正在构建一个 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;}
}
当我需要检索 Project
的 Description
和 Project
中所有 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));
}
}
}