Click here to Skip to main content
15,884,353 members
Please Sign up or sign in to vote.
3.00/5 (2 votes)
See more:
Hi,

I have a search box which allows the user to type in a comma delimited search string against an enumerable of any type of object. Because of this requirement I have to use expression trees. Filtering based on just one property at a time is easy enough, but filtering across multiple properties using multiple search strings has proven to be a bit tricky.

Basically I need help generating the equivalent expression tree for the following code:
C#
List<string> searchStrings = new List<string>()
// user search strings gets populated...

bool foundAny = _MyDataOfAnyType.Any(x => searchStrings.Any(y => x.Property1.Contains(y)/* || x.Property2.Contains(y) <|| even more properties of that class...>*/));
Posted
Updated 1-Sep-14 2:52am
v2

If I'm reading your question right, I don't think you need to use an expression tree.

You can use reflection to inspect the properties of the instances in the _MyDataOfAnyType and use that to get the string value of any properties that return a string. Then, check the searchString if it .Contains the returned value of the property. And by extending that method to have a set of comparators capable of comparing all the different type-sets that makes sense in your application neither the properties nor the search criteria has to be string values.

This example shows what I mean;

C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApplication1
{
    public abstract class SearchComparator
    {
        public abstract bool IsApplicable(object searchValue, object actualValue);
        public abstract bool IsMatch(object searchValue, object actualValue);
    }

    public abstract class ComparatorBase<T1, T2> : SearchComparator
    {
        protected abstract bool IsMatch(T1 searchValue, T2 actualValue);
        
        public override bool IsApplicable(object searchValue, object actualValue)
        {
            return searchValue is T1 && actualValue is T2;
        }

        public override bool IsMatch(object searchValue, object actualValue)
        {
            return IsMatch((T1)searchValue, (T2)actualValue);
        }
    }


    public class StringToPartialString : ComparatorBase<string, string>
    {
        protected override bool IsMatch(string searchValue, string actualValue)
        {
            return actualValue.Contains(searchValue);
        }
    }

    public class StringToPartialDecimal : ComparatorBase<string, decimal>
    {
        protected override bool IsMatch(string searchValue, decimal actualValue)
        {
            return actualValue.ToString().Contains(searchValue);
        }
    }

    public class DecimalToDecimal : ComparatorBase<decimal, decimal>
    {
        protected override bool IsMatch(decimal searchValue, decimal actualValue)
        {
            return searchValue == actualValue;
        }
    }

    public class ClassA
    {
        public string PropertyA { get; set; }
        public bool PropertyB { get; set; }
        public int PropertyC { get; set; }
        public string PropertyD { get; set; }
    }

    public class ClassB
    {
        public bool PropertyA { get; set; }
        public decimal PropertyB { get; set; }
        public string PropertyC { get; set; }
        public string PropertyD { get; set; }
    }

    public class Program
    {

        static IEnumerable<object> GetProperties(object target)
        {
            IList<object> values = new List<object>();
            foreach(var property in target.GetType().GetProperties())
            {
                // Try-Catch here in case the getter chucks an exception
                try {
                    var value = property.GetValue(target);
                    values.Add(property.GetValue(target));
                }
                catch {
                }
            }
            return values;
        }


        static IEnumerable<object> Search(IEnumerable<object> dataOfAnyType, IEnumerable<SearchComparator> comparators, IEnumerable<object> searchCriterion)
        {
            ISet<object> matches = new HashSet<object>();

            foreach(var data in dataOfAnyType)
            {
                foreach(var propertyValue in GetProperties(data))
                {
                    if (propertyValue != null)
                        System.Diagnostics.Debug.WriteLine("Looging for {0}", propertyValue.GetType().FullName);
                    foreach(var criteria in searchCriterion)
                    {
                        foreach(var comparator in comparators.Where(comp => comp.IsApplicable(criteria, propertyValue)))
                        {
                            if (comparator.IsMatch(criteria, propertyValue))
                                matches.Add(data);
                        }
                    }
                }
            }

            return matches;
        }

        static void Main(string[] args)
        {
            // This holds one instance for each type-pair that your application can compare
            var comparators = new SearchComparator[] {
                new StringToPartialString(),
                new StringToPartialDecimal(),
                new DecimalToDecimal()
            };

            var searchCriterion = new List<object> {
                123.2312m,
            };

            var _MyDataOfAnyType = new object[] {
                new ClassA { PropertyA = "Hello", PropertyD = "no data"},
                new ClassA(),
                new ClassB { PropertyC = "123" },
                new ClassB { PropertyB = 123.2312m },
                new ClassB { PropertyA = true, PropertyD = "World!"}
            };

            var matches = Search(_MyDataOfAnyType, comparators, searchCriterion);

            var foundAny = matches.Any();

            Console.WriteLine("Found {0} matching objects!", matches.Count());

        }
    }
}


Hope this helps,
Fredrik
 
Share this answer
 
v2
Comments
nortee 1-Sep-14 9:11am    
Hi Fredrik,

Unfortunately, I have to use expression trees. I will have no control over what properties are going to be searched as they are not limited to strings. I just used strings in this case because that is what I am going to be implementing first with the other types following later.

Also, the search strings are *partial* matches of a given properties value and not a complete match of the property value.
Fredrik Bornander 1-Sep-14 10:30am    
Made it more versatile to cover your requirements.
I think it's going to be difficult to use expression trees if you want the user to be able to enter "123" and have that match (for example) a decimal property with value 1234.
nortee 1-Sep-14 15:24pm    
Hi Frederik,

Thank you so much for your proposed solution. It does look promising, but I have some other issues to take into consideration (nested properties to name but a few). I am looking at both yours as well as Richard's solutions to work on something that might help me with my conundrum. I will be keeping both of you in the loop as what (combination) works best.
nortee 1-Sep-14 15:32pm    
Also, the logic I am using already works with int/decimal/double values that I will invoke based on the solution I will come up with from here :)... That in itself is the least of my problems :)
nortee 2-Sep-14 19:36pm    
Hi Frederick,

The method you describe works well when searching across all boards (when no specific type is defined). I am looking at going onto a scenario described below to resolve my issue but your help also was invaluable in what I wanted to do. Thank you so much for your time and effort!!!
There's a couple of approaches you could use here.

If you want to build the expression tree every time:
C#
private static Expression<Func<T, bool>> BuildSearchPredicate<T>(
    IEnumerable<string> searchStrings, 
    params string[] propertiesToSearch)
{
    var x = Expression.Parameter(typeof(T), "x");
    var properties = propertiesToSearch.Select(p => Expression.Property(x, p)).ToList();
    var containsMethod = typeof(string).GetMethod("Contains");

    var body = searchStrings
        .Select(y => Expression.Constant(y, typeof(string)))
        .SelectMany(y => properties, (y, p) => Expression.Call(p, containsMethod, y))
        .Aggregate<Expression>(Expression.OrElse);

    return Expression.Lambda<Func<T, bool>>(body, x);
}

// x => x.Property1.Contains("item1") || x.Property2.Contains("item1") || x.Property1.Contains("item2") || ...


If you want a reusable expression tree where you pass in the strings to find:
C#
private static Expression<Func<T, IEnumerable<string>, bool>> BuildSearchPredicate<T>(
    params string[] propertiesToSearch)
{
    var x = Expression.Parameter(typeof(T), "x");
    var searchStrings = Expression.Parameter(typeof(IEnumerable<string>), "searchStrings");

    var y = Expression.Parameter(typeof(string), "y");
    var containsMethod = typeof(string).GetMethod("Contains");

    var innerBody = propertiesToSearch
        .Select(p => Expression.Property(x, p))
        .Select(p => Expression.Call(p, containsMethod, y))
        .Aggregate<Expression>(Expression.OrElse);

    var innerLambda = Expression.Lambda<Func<string, bool>>(innerBody, y);

    var outerBody = Expression.Call(typeof(Enumerable), 
        "Any", new[] { typeof(string) }, 
        searchStrings, innerLambda);
    
    return Expression.Lambda<Func<T, IEnumerable<string>, bool>>(
        outerBody, x, searchStrings);
}

// (x, searchStrings) => searchStrings.Any(y => x.Property1.Contains(y) || x.Property2.Contains(y) || ...)
 
Share this answer
 
Comments
nortee 1-Sep-14 10:35am    
Thanks Richard,

This is more to what I am looking for. I will test this and report back.
nortee 1-Sep-14 15:30pm    
Hi Richard,

I didn't manage to finish my testing, but i did find that there was a missing ".OfType<expression>()" part before the .Aggregate line.

I am still testing and will keep you guys updating.

Thank you both SO much for your help!!!
Richard Deeming 1-Sep-14 15:32pm    
You shouldn't need an .OfType<Expression>() call - by specifying the type argument for the .Aggregate<Expression>() call, it should implicitly convert the arguments to the <codde>Expression class.
nortee 1-Sep-14 15:36pm    
Hi Richard,

Maybe it's my version of .net... but it definitely needed that or else my code would not compile. When I added in that line, then it worked/compiled. bloody versioning!!! XD
nortee 2-Sep-14 19:33pm    
Hi Richard,

I have found a solution to my problem. It is a combination of what you provided to me as well as some adjustments I made. I will update accordingly when I am back at the office.

Once again, thank you so much for you time. It was invaluable!!!!

This content, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900