Search code examples
c#generics.net-corelambdaexpression-trees

Build a Generic Expression Tree .NET Core


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

Solution

  • 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.