构建通用表达式树 .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>>?

https://www.strathweb.com/2018/01/easy-way-to-create-a-c-lambda-expression-from-a-string-with-roslyn/

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){...}

目前为止效果很好。