1

Forgive me, I'm not entirely sure my question is worded correctly. I'm creating a search component where the user can search different fields with different operators... e.g. description.contains(keywords) and measurement.startsWith(yards).....

So here is what I have:

void SearchDescription(IQueryable<MyClass> results, string keywords)
{
    switch(operator)
    { 
        case "Contains":
            results=results.Where(ele => ele.description.Contains(keywords));
            break;
        case "StartsWith":
            results = results.Where(ele => ele.description.StartsWith(keywords));
            break;
        ... and so on.....
    }
}

Currently I have a method just as above for each field.... SearchDescription(), SearchID(), SearchMeasure(), etc. The only difference being the field/property name.

UPDATE

Upon further research possibly something like:

void Search(IQueryable<Entity> results, string keywords, Expression<Func<Entity>,object>> predicate)
{
    results = results.Where(ele => predicate.Contains(keywords));
}

which could be called like:

Search(results, "my search terms", ele => ele.description); 

This obviously doesn't work in it's current form, but maybe that is a clearer description of what I am after.

Thanks for all the responses so far.

Matt
  • 13
  • 4
  • Take a look here http://stackoverflow.com/a/10283288/1300049. Or you can define few actions to get needed properties and pass them (Actions) to your Search method – JleruOHeP Feb 27 '14 at 22:13
  • You might look at some of the Dynamic Linq options out there. Some use reflection and some build expression trees on the fly. It's not as straight-forward as you're hoping though, as far as I can see. – Erik Noren Feb 27 '14 at 22:18

2 Answers2

0

You can use System.Reflection to retrieve a PropertyInfo using the name of the wanted property. Note that Reflection is a bit slow and if used many times in a second, it can seriously slow your program down.

void Search(IQueryable<MyClass> results, string keywords, string propertyName)
{
    PropertyInfo elePropInfo = ele.GetType().GetProperty(propertyName);
    string elePropValue = (string)elePropInfo.GetValue(ele, null); // the second argument should be null for non-indexed properties
    switch(operator)
    { 
        case "Contains":
            results = results.Where(ele => elePropValue.Contains(keywords));
            break;
        case "StartsWith":
            results = results.Where(ele => elePropValue.StartsWith(keywords));
            break;
        // etc
    }
}

More info on GetProperty() can be found here in MSDN: http://msdn.microsoft.com/en-us/library/kz0a8sxy(v=vs.110).aspx

Aoi Karasu
  • 3,730
  • 3
  • 37
  • 61
Spans
  • 364
  • 7
  • 17
0

This can be done by implementing a Compose method that will take two expressions and return an expression that acts as if it would invoke the first, then provide that as the parameter to the second:

void Search(IQueryable<Entity> results, string keywords,
    Expression<Func<Entity, string>> selector)
{
    results = results.Where(selector.Compose(obj => obj.Contains(keywords)));
}

To implement that we'll start off with a helper method that allows us to replace all instances of one expression with another:

internal class ReplaceVisitor : ExpressionVisitor
{
    private readonly Expression from, to;
    public ReplaceVisitor(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    public override Expression Visit(Expression node)
    {
        return node == from ? to : base.Visit(node);
    }
}

public static Expression Replace(this Expression expression,
    Expression searchEx, Expression replaceEx)
{
    return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}

Using that tool it's as simple as a handful of replacements stuffed back together into a lambda:

public static Expression<Func<TFirstParam, TResult>>
    Compose<TFirstParam, TIntermediate, TResult>(
    this Expression<Func<TFirstParam, TIntermediate>> first,
    Expression<Func<TIntermediate, TResult>> second)
{
    var param = Expression.Parameter(typeof(TFirstParam), "param");

    var newFirst = first.Body.Replace(first.Parameters[0], param);
    var newSecond = second.Body.Replace(second.Parameters[0], newFirst);

    return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
}

It also seems odd to filter the query to items where a single string with all of the words is contained in the given field. It seems more likely that you want to get items that contain any of a list of strings. That's different, and requires just a touch more work.

We can use a new class we'll call a PredicateBuilder to build up a filter that takes the logical OR of a bunch of other filters.

void Search(IQueryable<Entity> results, IEnumerable<string> keywords,
    Expression<Func<Entity, string>> selector)
{
    var finalFilter = keywords.Aggregate(
        PredicateBuilder.False<Entity>(),
        (filter, keyword) => filter.Or(
            selector.Compose(obj => obj.Contains(keyword))));
    results = results.Where(finalFilter);
}

We can implement this class using the Replace method defined earlier like so:

public static class PredicateBuilder
{
    public static Expression<Func<T, bool>> True<T>() { return f => true; }
    public static Expression<Func<T, bool>> False<T>() { return f => false; }

    public static Expression<Func<T, bool>> Or<T>(
        this Expression<Func<T, bool>> expr1,
        Expression<Func<T, bool>> expr2)
    {
        var secondBody = expr2.Body.Replace(expr2.Parameters[0], expr1.Parameters[0]);
        return Expression.Lambda<Func<T, bool>>
              (Expression.OrElse(expr1.Body, secondBody), expr1.Parameters);
    }

    public static Expression<Func<T, bool>> And<T>(
        this Expression<Func<T, bool>> expr1,
        Expression<Func<T, bool>> expr2)
    {
        var secondBody = expr2.Body.Replace(expr2.Parameters[0], expr1.Parameters[0]);
        return Expression.Lambda<Func<T, bool>>
              (Expression.AndAlso(expr1.Body, secondBody), expr1.Parameters);
    }
}
Servy
  • 202,030
  • 26
  • 332
  • 449