Build a Generic Expression Tree .NET Core

二次信任 提交于 2021-01-28 05:16:59

问题


Hello Community i am aware of this might be a possible duplicate.

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

There are obviously too many resources.

I am still confused though. Could someone provide a clearer picture of what is happening in the below code. Below i have provided some comments to help my understanding.

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;
    }

The above obviously is not working.

This is returned to linq query like this IEnumerable<SomeModel>.Where(ParseParametersToFilter.Compile())

I understand my mistake is a casting mistake. How could i fix this?

After @Jeremy Lakeman answer i updated my code to look like this. Although the ViewModel i am using is quite complex. I have provided a small preview at the end.

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

But now i get the following Exceptions

When i pass the Status as property parameter:

The binary operator Equal is not defined for the types 'model.StatusEnum' and 'System.String'.

When i pass the User.FriendlyName parameter:

Instance property 'User.FriendlyName' is not defined for type 'model.ReportViewModel' Parameter name: propertyName

Here is how my view model looks like!

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;}
}

回答1:


So you're trying to turn something like "a==1,b==3" into viewModel => viewModel.a == 1 && viewModel.b == 3?

I think you're already pretty close, you just need add the && (or ||), and always create a 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);
    }

And if this is for passing to entity framework, don't compile it or you'll only be able to evaluate it client side.




回答2:


Here is what i came up with and works pretty well, from my tests today. Some refactoring may be needed. I am open to suggestions.

Please make sure to check the comments inside the code.

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

This worked very good so far.



来源:https://stackoverflow.com/questions/61798761/build-a-generic-expression-tree-net-core

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!