构建通用表达式树 .NET Core
Build a Generic Expression Tree .NET Core
大家好,我知道这可能是重复的。
How do I dynamically create an Expression<Func<MyClass, bool>> predicate from Expression<Func<MyClass, string>>?
How to create a Expression.Lambda when a type is not known until runtime?
Creating expression tree for accessing a Generic type's property
资源明显太多了
虽然我仍然很困惑。
有人可以更清楚地了解以下代码中发生的情况。
下面我提供了一些评论来帮助我理解。
private Expression<Func<T, bool>> ParseParametersToFilter<T>(string parameters)
{
Expression<Func<T, bool>> finalExpression = Expression.Constant(true); //Casting error
if (string.IsNullOrEmpty(parameters))
return finalExpression;
string[] paramArray = parameters.Split(","); //parameters is one string splitted with commas
ParameterExpression argParam = Expression.Parameter(typeof(T), "viewModel"); //Expression Tree
foreach (var param in paramArray)
{
var parsedParameter = ParseParameter(param);
if (parsedParameter.operation == Operation.None)
continue; // this means we parsed incorrectly we TODO: Better way for error handling
//Property might be containment property e.g T.TClass.PropName
Expression nameProperty = Expression.Property(argParam, parsedParameter.propertyName);
//Value to filter against
var value = Expression.Constant(parsedParameter.value);
Expression comparison;
switch (parsedParameter.operation)
{ //Enum
case Operation.Equals:
comparison = Expression.Equal(nameProperty, value);
break;
//goes on for NotEquals, GreaterThan etc
}
finalExpression = Expression.Lambda(comparison, argParam);// Casting error
}
return finalExpression;
}
以上显然是行不通的。
这是像这样返回到 linq 查询 IEnumerable<SomeModel>.Where(ParseParametersToFilter.Compile())
我明白我的错误是铸造错误。
我该如何解决这个问题?
@Jeremy Lakeman 回答后,我将代码更新为如下所示。尽管我使用的 ViewModel 非常复杂。我在最后提供了一个小预览。
private Expression<Func<T, bool>> ParseParametersToFilter<T>(string parameters)
{
Expression<Func<T, bool>> finalExpression = t => true;
if (string.IsNullOrEmpty(parameters))
return finalExpression;
string[] paramArray = parameters.Split(","); //parameters is one string splitted with commas
ParameterExpression argParam = Expression.Parameter(typeof(T), "viewModel"); //Expression Tree
Expression body = Expression.Constant(true);
foreach (var param in paramArray)
{
var parsedParameter = ParseParameter(param);
if (parsedParameter.operation == Operation.None)
continue; // this means we parsed incorrectly TODO: Better way for error handling
//Property might be containment property e.g T.TClass.PropName
Expression nameProperty = Expression.Property(argParam, parsedParameter.propertyName);
//Value to filter against
var value = Expression.Constant(parsedParameter.value);
switch (parsedParameter.operation)
{ //Enum
case Operation.Equals:
body = Expression.AndAlso(body, Expression.Equal(nameProperty, value));
break;
//goes on for NotEquals, GreaterThan etc
}
body = Expression.AndAlso(body, argParam);
}
return Expression.Lambda<Func<T, bool>>(body, argParam);
}
private (string propertyName, Operation operation, string value) ParseParameter(string parameter){...}
但现在我得到以下异常
当我将状态作为 属性 参数传递时:
The binary operator Equal is not defined for the types 'model.StatusEnum' and 'System.String'.
当我传递 User.FriendlyName 参数时:
Instance property 'User.FriendlyName' is not defined for type 'model.ReportViewModel'
Parameter name: propertyName
这是我的视图模型的样子!
public class ReportViewModel
{
public StatusEnum Status {get;set;}
public UserViewModel User {get;set;}
}
public enum StatusEnum
{
Pending,
Completed
}
public class UserViewModel
{
public string FriendlyName {get;set;}
}
所以你想把 "a==1,b==3"
变成 viewModel => viewModel.a == 1 && viewModel.b == 3
?
我认为你已经很接近了,你只需要添加 &&
(或 ||
),并始终创建一个 lambda;
private Expression<Func<T, bool>> ParseParametersToFilter<T>(string parameters)
{
ParameterExpression argParam = Expression.Parameter(typeof(T), "viewModel"); //Expression Tree
Expression body = Expression.Constant(true);
if (!string.IsNullOrEmpty(parameters)){
body = parameters.Split(",")
.Select(param => {
var parsedParameter = ParseParameter(param);
// ... as above, turn param into a comparison expression ...
return comparison;
})
.Aggregage((l,r) => Expression.AndAlso(l, r));
}
return Expression.Lambda<Func<T, bool>>(body, argParam);
}
如果这是为了传递给 entity framework,请不要编译它,否则您将只能在客户端对其进行评估。
这是我今天的测试得出的并且效果很好。
可能需要进行一些重构。我乐于接受建议。
请务必检查代码中的注释。
private void ConvertValuePropertyType(Type type, string value, out dynamic converted)
{
// Here i convert the value to filter to the necessary type
// All my values come as strings.
if (type.IsEnum)
converted = Enum.Parse(type, value);
else if (type == typeof(DateTime))
converted = DateTime.Parse(value);
else if (type is object)
converted = value;
else
throw new InvalidCastException($"Value was not converted properly {nameof(value)} {nameof(type)}");
}
private MemberExpression GetContainmentMember(ParameterExpression parameterExpression, string propertyName)
{
//propertName looks like this User.FriendlyName
//So we have to first take T.User from the root type
// Then the Name property.
// I am not sure how to make this work for any depth.
var propNameArray = propertyName.Split(".");
if (propNameArray.Length > 1)
{
MemberExpression member = Expression.Property(parameterExpression, propNameArray[0]);
return Expression.PropertyOrField(member, propNameArray[1]);
}
else
{ //This needs to make sure we retrieve containment
return Expression.Property(parameterExpression, propertyName);
}
}
// ***************************************************************
// This is the core method!
private Expression<Func<T, bool>> ParseParametersToFilter<T>(string parameters)
{
Expression body = Expression.Constant(true);
ParameterExpression argParam = Expression.Parameter(typeof(T), nameof(T));
if (string.IsNullOrEmpty(parameters))
return Expression.Lambda<Func<T, bool>>(body, argParam); // return empty filter
string[] paramArray = parameters.Split(","); //parameters is one string splitted with commas
foreach (var param in paramArray)
{
var parsedParameter = ParseParameter(param);
if (parsedParameter.operation == Operation.None)
continue; // this means we parsed incorrectly, do not fail continue
//Get model
//Get property name
//Property might be containment property e.g T.TClass.PropName
//Value to filter against
MemberExpression nameProperty = GetContainmentMember(argParam, parsedParameter.propertyName);
//Convert property value according to property name
Type propertyType = GetPropertyType(typeof(T), parsedParameter.propertyName);
ConvertValuePropertyType(propertyType, parsedParameter.value, out object parsedValue);
var value = Expression.Constant(parsedValue);
switch (parsedParameter.operation)
{
//What operation did the parser retrieve
case Operation.Equals:
body = Expression.AndAlso(body, Expression.Equal(nameProperty, value));
break;
//goes on for NotEquals, GreaterThan etc
default:
break;
}
}
return Expression.Lambda<Func<T, bool>>(body, argParam);
}
private (string propertyName, Operation operation, string value) ParseParameter(string parameter){...}
目前为止效果很好。
大家好,我知道这可能是重复的。
How do I dynamically create an Expression<Func<MyClass, bool>> predicate from Expression<Func<MyClass, string>>?
How to create a Expression.Lambda when a type is not known until runtime?
Creating expression tree for accessing a Generic type's property
资源明显太多了
虽然我仍然很困惑。 有人可以更清楚地了解以下代码中发生的情况。 下面我提供了一些评论来帮助我理解。
private Expression<Func<T, bool>> ParseParametersToFilter<T>(string parameters)
{
Expression<Func<T, bool>> finalExpression = Expression.Constant(true); //Casting error
if (string.IsNullOrEmpty(parameters))
return finalExpression;
string[] paramArray = parameters.Split(","); //parameters is one string splitted with commas
ParameterExpression argParam = Expression.Parameter(typeof(T), "viewModel"); //Expression Tree
foreach (var param in paramArray)
{
var parsedParameter = ParseParameter(param);
if (parsedParameter.operation == Operation.None)
continue; // this means we parsed incorrectly we TODO: Better way for error handling
//Property might be containment property e.g T.TClass.PropName
Expression nameProperty = Expression.Property(argParam, parsedParameter.propertyName);
//Value to filter against
var value = Expression.Constant(parsedParameter.value);
Expression comparison;
switch (parsedParameter.operation)
{ //Enum
case Operation.Equals:
comparison = Expression.Equal(nameProperty, value);
break;
//goes on for NotEquals, GreaterThan etc
}
finalExpression = Expression.Lambda(comparison, argParam);// Casting error
}
return finalExpression;
}
以上显然是行不通的。
这是像这样返回到 linq 查询 IEnumerable<SomeModel>.Where(ParseParametersToFilter.Compile())
我明白我的错误是铸造错误。 我该如何解决这个问题?
@Jeremy Lakeman 回答后,我将代码更新为如下所示。尽管我使用的 ViewModel 非常复杂。我在最后提供了一个小预览。
private Expression<Func<T, bool>> ParseParametersToFilter<T>(string parameters)
{
Expression<Func<T, bool>> finalExpression = t => true;
if (string.IsNullOrEmpty(parameters))
return finalExpression;
string[] paramArray = parameters.Split(","); //parameters is one string splitted with commas
ParameterExpression argParam = Expression.Parameter(typeof(T), "viewModel"); //Expression Tree
Expression body = Expression.Constant(true);
foreach (var param in paramArray)
{
var parsedParameter = ParseParameter(param);
if (parsedParameter.operation == Operation.None)
continue; // this means we parsed incorrectly TODO: Better way for error handling
//Property might be containment property e.g T.TClass.PropName
Expression nameProperty = Expression.Property(argParam, parsedParameter.propertyName);
//Value to filter against
var value = Expression.Constant(parsedParameter.value);
switch (parsedParameter.operation)
{ //Enum
case Operation.Equals:
body = Expression.AndAlso(body, Expression.Equal(nameProperty, value));
break;
//goes on for NotEquals, GreaterThan etc
}
body = Expression.AndAlso(body, argParam);
}
return Expression.Lambda<Func<T, bool>>(body, argParam);
}
private (string propertyName, Operation operation, string value) ParseParameter(string parameter){...}
但现在我得到以下异常
当我将状态作为 属性 参数传递时:
The binary operator Equal is not defined for the types 'model.StatusEnum' and 'System.String'.
当我传递 User.FriendlyName 参数时:
Instance property 'User.FriendlyName' is not defined for type 'model.ReportViewModel' Parameter name: propertyName
这是我的视图模型的样子!
public class ReportViewModel
{
public StatusEnum Status {get;set;}
public UserViewModel User {get;set;}
}
public enum StatusEnum
{
Pending,
Completed
}
public class UserViewModel
{
public string FriendlyName {get;set;}
}
所以你想把 "a==1,b==3"
变成 viewModel => viewModel.a == 1 && viewModel.b == 3
?
我认为你已经很接近了,你只需要添加 &&
(或 ||
),并始终创建一个 lambda;
private Expression<Func<T, bool>> ParseParametersToFilter<T>(string parameters)
{
ParameterExpression argParam = Expression.Parameter(typeof(T), "viewModel"); //Expression Tree
Expression body = Expression.Constant(true);
if (!string.IsNullOrEmpty(parameters)){
body = parameters.Split(",")
.Select(param => {
var parsedParameter = ParseParameter(param);
// ... as above, turn param into a comparison expression ...
return comparison;
})
.Aggregage((l,r) => Expression.AndAlso(l, r));
}
return Expression.Lambda<Func<T, bool>>(body, argParam);
}
如果这是为了传递给 entity framework,请不要编译它,否则您将只能在客户端对其进行评估。
这是我今天的测试得出的并且效果很好。 可能需要进行一些重构。我乐于接受建议。
请务必检查代码中的注释。
private void ConvertValuePropertyType(Type type, string value, out dynamic converted)
{
// Here i convert the value to filter to the necessary type
// All my values come as strings.
if (type.IsEnum)
converted = Enum.Parse(type, value);
else if (type == typeof(DateTime))
converted = DateTime.Parse(value);
else if (type is object)
converted = value;
else
throw new InvalidCastException($"Value was not converted properly {nameof(value)} {nameof(type)}");
}
private MemberExpression GetContainmentMember(ParameterExpression parameterExpression, string propertyName)
{
//propertName looks like this User.FriendlyName
//So we have to first take T.User from the root type
// Then the Name property.
// I am not sure how to make this work for any depth.
var propNameArray = propertyName.Split(".");
if (propNameArray.Length > 1)
{
MemberExpression member = Expression.Property(parameterExpression, propNameArray[0]);
return Expression.PropertyOrField(member, propNameArray[1]);
}
else
{ //This needs to make sure we retrieve containment
return Expression.Property(parameterExpression, propertyName);
}
}
// ***************************************************************
// This is the core method!
private Expression<Func<T, bool>> ParseParametersToFilter<T>(string parameters)
{
Expression body = Expression.Constant(true);
ParameterExpression argParam = Expression.Parameter(typeof(T), nameof(T));
if (string.IsNullOrEmpty(parameters))
return Expression.Lambda<Func<T, bool>>(body, argParam); // return empty filter
string[] paramArray = parameters.Split(","); //parameters is one string splitted with commas
foreach (var param in paramArray)
{
var parsedParameter = ParseParameter(param);
if (parsedParameter.operation == Operation.None)
continue; // this means we parsed incorrectly, do not fail continue
//Get model
//Get property name
//Property might be containment property e.g T.TClass.PropName
//Value to filter against
MemberExpression nameProperty = GetContainmentMember(argParam, parsedParameter.propertyName);
//Convert property value according to property name
Type propertyType = GetPropertyType(typeof(T), parsedParameter.propertyName);
ConvertValuePropertyType(propertyType, parsedParameter.value, out object parsedValue);
var value = Expression.Constant(parsedValue);
switch (parsedParameter.operation)
{
//What operation did the parser retrieve
case Operation.Equals:
body = Expression.AndAlso(body, Expression.Equal(nameProperty, value));
break;
//goes on for NotEquals, GreaterThan etc
default:
break;
}
}
return Expression.Lambda<Func<T, bool>>(body, argParam);
}
private (string propertyName, Operation operation, string value) ParseParameter(string parameter){...}
目前为止效果很好。