Search code examples
c#expression-trees

How do I create an expression tree to represent 'p=>p.a.tostring().Contains("s")' in C#?


I have IQueryable type of data collection, I abstracted a method, used as a full-field fuzzy query, at the beginning, I was one by one attribute name to write. Like the following code:

private IQueryable<Tables1> FilterResult(string search, List<Tables1> dtResult)
    {
        IQueryable<Tables1> results = dtResult.AsQueryable();
        results = results.Where(p => (
            search == null || (
                p.Name != null && p.Name.Contains(search) ||
                p.age != null && p.age.ToString().Contains(search) ||
                p.sex != null && p.sex.Contains(search) ||
                p.content1 != null && p.content1.Contains(search) ||
                p.content2 != null && p.content2.Contains(search) ||
                p.content3 != null && p.content3.Contains(search)
                )
            ));
        return results;
    }

But this write if the type of incoming List collection changes, then all the physical attributes have to re-write. So I changed the type of T:

private IQueryable<T> FilterResult(string search, List<T> dtResult,T t)
    {
        IQueryable<T> results = dtResult.AsQueryable();
        //do something
        return results;
    }

The idea behind this is to get all the attributes of the incoming T type by reflection. Then construct the Lambda expression through Expression Tree.

The question is how do I construct a 'p => p.age.ToString ().Contains(search)'by Expression Tree?

The following is the complete code:

    private IQueryable<T> FilterResult(string search, List<T> dtResult, T t) 
    {
        List<Expression> tempExp = new List<Expression>();
        var parameter = Expression.Parameter(typeof(T), "p");
        foreach (var mi in t.GetType().GetProperties())
        {
            Expression left = Expression.Property(parameter, t.GetType().GetProperty(mi.Name));
            Expression right = Expression.Constant(search, typeof(string));
            MethodInfo method;
            MethodCallExpression exp;
            if (mi.PropertyType == typeof(Int32) || mi.PropertyType == typeof(Int64))
            {
                //this code is wrong
                var exp1 = Expression.Call(Expression.Convert(left, typeof(string)), typeof(object).GetMethod("ToString"));
                method = typeof(string).GetMethod("Contains", new[] { typeof(string) });
                exp = Expression.Call(exp1, method, right);

            }
            else
            {
                method = typeof(string).GetMethod("Contains", new[] { typeof(string) });
                exp = Expression.Call(left, method, right);
            }
            tempExp.Add(exp);
        }
        Expression all = Expression.Or(Expression.Equal(Expression.Constant(search), null), tempExp[0]);
        for (int i = 1; i < tempExp.Count; i++)
        {
            all = Expression.Or(all, tempExp[i]);
        }
        var lambda = Expression.Lambda<Func<T, bool>>(all, parameter);
        var results = dtResult.Where(lambda.Compile()).AsQueryable(); ;
        return results;
    }

Solution

  • I'd say it can be easier. I've simplified your Tables model for presentation.

    public class Tables
    {
        public int Age { get; set; }
        public string Name { get; set; }
        public string Content { get; set; }
    }
    
    public class Matcher
    {
        private static readonly PropertyInfo[] Properties = typeof(Tables).GetRuntimeProperties().ToArray();
    
        public IQueryable<Tables> FilterResult(string search, List<Tables> dtResult)
        {   
            if(search == null) //Consider using string.IsNullOrWhiteSpace(search) but I wasn't sure if you want to avoid searching for spaces
            {
                return dtResult.AsQueryable();
            }
            return dtResult.Where(p => IsMatch(p, search)).AsQueryable();
        }
    
        private static bool IsMatch(Tables tables, string search)
        {
            foreach (var propertyInfo in Properties)
            {
                var value = propertyInfo.GetValue(tables);
                if (value != null && value.ToString().Contains(search))
                {
                    return true;
                }
            }
    
            return false;
        }
    }
    

    And here we can put it into work:

    class Program
    {
        public static void Main()
        {
            const string search = "Bob";
            var matcher = new Matcher();
            var items = new List<Tables>
            {
                new Tables {Content = string.Empty, Name = "Bob"}, //This will match
                new Tables {Content = "Bob is the best guy.", Name = "Joe"}, //This will also match
                new Tables {Content = "Something", Name = null} // This won't null name to verify that nothing unexpected will happen
            };
    
            var results = matcher.FilterResult( search, items );
            foreach ( var result in results )
            {
                Console.WriteLine($"Matched the guy named {result.Name}");
            }
            Console.ReadKey();
        }
    }
    

    Edit: Here's the generic version

    public class Matcher<T>
    {
        private static readonly PropertyInfo[] Properties = typeof(T).GetRuntimeProperties().ToArray();
    
        public IQueryable<T> FilterResult(string search, List<T> items)
        {
            if ( search == null) //Consider using string.IsNullOrWhiteSpace(search) but I wasn't sure if you want to avoid searching for spaces
            {
                return items.AsQueryable();
            }
    
            return items.Where(p => IsMatch(p, search)).AsQueryable();
        }
    
        private static bool IsMatch(T item, string search)
        {
            foreach (var propertyInfo in Properties)
            {
                var value = propertyInfo.GetValue(item);
                if (value != null && value.ToString().Contains(search))
                {
                    return true;
                }
            }
    
            return false;
        }
    }