Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically create lambda search on IQueryable on nested objects

I'm trying to build a dynamic search on nested objects, which will later be sent to EF and SQL Server. So far, I'm able to search on all properties of the first object. Here's a very simplified version:

public class User
{
    public string Name { get; set; }
    public Address Address { get; set; }
}
public class Address
{
    public string City { get; set; }
}

public class MyClass<TEntity> where TEntity : class {
    public IQueryable<TEntity> applySearch(IQueryable<TEntity> originalList, string propName, string valueToSearch) {

        ParameterExpression parameterExpression = Expression.Parameter(typeof(TEntity), "p");
        PropertyInfo propertyInfo = typeof(TEntity).GetProperty(propName);
        MemberExpression member = Expression.MakeMemberAccess(parameterExpression, propertyInfo);
        lambda = Expression.Lambda<Func<TEntity, bool>>(Expression.Equal(member, Expression.Constant(valueToSearch)), parameterExpression);

        return originalList.Where(expression);
    }
}

When propName = "Name" everything is fine, but when propName = "Address.City", the propertyInfo is null, and I get this error on the member assignment line:

System.ArgumentNullException: Value cannot be null

I was able to obtain the propertyInfo of the nested property using the solution from this answer:

PropertyInfo propertyInfo = GetPropertyRecursive(typeof(TEntity), propName);
...

private PropertyInfo GetPropertyRecursive(Type baseType, string propertyName)
{
    string[] parts = propertyName.Split('.');

    return (parts.Length > 1)
        ? GetPropertyRecursive(baseType.GetProperty(parts[0]).PropertyType, parts.Skip(1).Aggregate((a, i) => a + "." + i))
        : baseType.GetProperty(propertyName);
}

But then I get this error on member assignment:

System.ArgumentException: Property 'System.String City' is not defined for type 'User'

This should point to Address instead of User, but I don't know if I'm on right track here, I mean, should I change parameterExpression now?

How can I make a dynamic search on nested objects, so that this can be turned into a lambda expression and later sent to SQL?

like image 659
Marcos Dimitrio Avatar asked Jul 27 '15 05:07

Marcos Dimitrio


2 Answers

After Kobi's advice and a lot of trial and error, I finally got this working. This uses the Universal PredicateBuilder. Here it is:

public class MyClass<TEntity> where TEntity : class
{
    public IQueryable<TEntity> ApplySearch(IQueryable<TEntity> originalList, string valueToSearch, string[] columnsToSearch)
    {

        Expression<Func<TEntity, bool>> expression = null;

        foreach (var propName in columnsToSearch)
        {
            Expression<Func<TEntity, bool>> lambda = null;

            ParameterExpression parameterExpression = Expression.Parameter(typeof(TEntity), "p");

            string[] nestedProperties = propName.Split('.');
            Expression member = parameterExpression;
            foreach (string prop in nestedProperties)
            {
                member = Expression.PropertyOrField(member, prop);
            }

            var canConvert = CanConvertToType(valueToSearch, member.Type.FullName);

            if (canConvert)
            {
                var value = ConvertToType(valueToSearch, member.Type.FullName);
                if (member.Type.Name == "String")
                {
                    ConstantExpression constant = Expression.Constant(value);
                    MethodInfo mi = typeof(string).GetMethod("StartsWith", new Type[] { typeof(string) });
                    Expression call = Expression.Call(member, mi, constant);

                    lambda = Expression.Lambda<Func<TEntity, bool>>(call, parameterExpression);
                }
                else
                {
                    lambda = Expression.Lambda<Func<TEntity, bool>>(Expression.Equal(member, Expression.Constant(value)), parameterExpression);
                }
            }

            if (lambda != null)
            {
                if (expression == null)
                {
                    expression = lambda;
                }
                else
                {
                    expression = expression.Or(lambda);
                }
            }
        }

        if (expression != null)
        {
            return originalList.Where(expression);
        }

        return originalList;
    }
}

private bool CanConvertToType(object value, string type)
{
    bool canConvert;
    try
    {
        var cValue = ConvertToType(value, type);
        canConvert = true;
    }
    catch
    {
        canConvert = false;
    }
    return canConvert;
}

private dynamic ConvertToType(object value, string type)
{
    return Convert.ChangeType(value, Type.GetType(type));
}
like image 166
Marcos Dimitrio Avatar answered Oct 16 '22 20:10

Marcos Dimitrio


Warning in advance - I'm not building the expression, just inspecting its structure.

When I need to dynamically create Expressions, I find it useful to inspect an Expression and copy its structure:

Expression<Func<User, string>> getCity = user => user.Address.City;

Now you can simply debug it, for example in the immediate window (ctrlalti here):

getCity
{user => user.Address.City}
    Body: {user.Address.City}
    CanReduce: false
    DebugView: ".Lambda #Lambda1<System.Func`2[ConsoleApplication1.User,System.String]>(ConsoleApplication1.User $user) {\r\n    ($user.Address).City\r\n}"
    Name: null
    NodeType: Lambda
    Parameters: Count = 1
    ReturnType: {Name = "String" FullName = "System.String"}
    TailCall: false

Here we can see getCity is a Lambda with one parameter. Let's inspect it's body:

getCity.Body
{user.Address.City}
    CanReduce: false
    DebugView: "($user.Address).City"
    Expression: {user.Address}
    Member: {System.String City}
    NodeType: MemberAccess
    Type: {Name = "String" FullName = "System.String"}

getCity.Body is a member access - it accesses the member City of the Expression user.Address. Technically that's a PropertyExpression, which is an internal class so we can't even cast to it, but that's OK.
Finally, let's look at that inner expression:

((MemberExpression)getCity.Body).Expression
{user.Address}
    CanReduce: false
    DebugView: "$user.Address"
    Expression: {user}
    Member: {ConsoleApplication1.Address Address}
    NodeType: MemberAccess
    Type: {Name = "Address" FullName = "ConsoleApplication1.Address"}

That's just user.Address.

Now we can build an identical expression:

var addressProperty = typeof (User).GetProperty("Address");
var cityProperty = typeof(Address).GetProperty("City");
var userParameter = Expression.Parameter(typeof (User), "user");
var getCityFromUserParameter = Expression.Property(Expression.Property(userParameter, addressProperty), cityProperty);
var lambdaGetCity = Expression.Lambda<Func<User, string>>(getCityFromUserParameter, userParameter);

Expression.MakeMemberAccess works too, instead of Expression.Property.

Obviously, you'd need to build your expression in a loop, and more dynamically, but the structure is the same.

like image 38
Kobi Avatar answered Oct 16 '22 18:10

Kobi