1

I'm constructing a simple search function for a site. The aim is to allow the user to find 'swimmers' and add them to a list via a search box, using a query like "bob staff".

The first part of this I decided to tackle was allowing the user to search via a group name (In the database all swimmers are part of a group). The following is the code I have at the moment.

[HttpGet]
    public JsonResult searchForSwimmers(string q)
    {
        //designed to return [{name='foo',id='bar'}]

        String[] QueryTerms = q.Split(' '); //all the search terms are sep. by " "
        var groupResults = _db.SwimGroups.Where(g => g.Name.ContainsAny(QueryTerms)) 
            .OrderByDescending(g => g.Name.StartsWithAny(QueryTerms) ? 1 : 0)
            .ThenBy( g => g)
            .Select(g => new { name = g.Name, id = g.ID });


        return Json(groupResults,JsonRequestBehavior.AllowGet);
    }

On line 8, there is a method invoked called StartsWithAny. This is an extension method I defined in the following file:

public static class StringUtils
    {
     public static Boolean StartsWithAny(this String str, params String[] Fragments)
        {
            foreach (String fragment in Fragments)
            {
                if (str.StartsWith(fragment))
                {
                    return true;
                }
            }
            return false;
        }
    }

The idea is that if a Name starts with one of the terms then it should be ranked higher in relevancy. I recognize that this logic is naïve and has flaws however I thought it would be a good example to illustrate the problem I've having. The code compiles however when searchForSimmers is invoked in my cshtml page with the following: (using the tokenInput library)

<script type="text/javascript">
     $(document).ready(function () {
          $("#demo-input-local").tokenInput("/Admin/searchForSwimmers");
     });
</script>

I get a 500 internal server error. The error message is as follows:

LINQ to Entities does not recognize the method 'Boolean ContainsAny(System.String, System.String[])' method, and this method cannot be translated into a store expression.

The ContainsAny method

public static Boolean ContainsAny(this String str, List<String> Fragments)
        {
            foreach (String fragment in Fragments)
            {
                if(str.Contains(fragment))
                {
                    return true;
                }
            }
            return false;
        }

I've had a look around but couldn't find a solution to the problem. Any help would be greatly appreciated, cheers.

tereško
  • 58,060
  • 25
  • 98
  • 150
HennyH
  • 7,794
  • 2
  • 29
  • 39

2 Answers2

4

Check out my blog where I create a search extension method for IQueryable.

UPDATED: I have created a new blog post showing the extension method required to acheive your goal

http://www.ninjanye.co.uk/2013/04/generic-iqueryable-or-search-for.html

http://jnye.co/Posts/8/generic-iqueryable-or-search-for-multiple-search-terms-using-expression-trees

The problem is linq does not know how to translate your code into SQL.

By adding the following extension method:

public static class QueryableExtensions
{
    public static IQueryable<T> Search<T>(this IQueryable<T> source, Expression<Func<T, string>> stringProperty, params string[] searchTerms)
    {
        if (!searchTerms.Any())
        {
            return source;
        }

        Expression orExpression = null;
        foreach (var searchTerm in searchTerms)
        {
            //Create expression to represent x.[property].Contains(searchTerm)
            var searchTermExpression = Expression.Constant(searchTerm);
            var containsExpression = BuildContainsExpression(stringProperty, searchTermExpression);

            orExpression = BuildOrExpression(orExpression, containsExpression);
        }

        var completeExpression = Expression.Lambda<Func<T, bool>>(orExpression, stringProperty.Parameters);
        return source.Where(completeExpression);
    }

    private static Expression BuildOrExpression(Expression existingExpression, Expression expressionToAdd)
    {
        if (existingExpression == null)
        {
            return expressionToAdd;
        }

        //Build 'OR' expression for each property
        return Expression.OrElse(existingExpression, expressionToAdd);
    }

    private static MethodCallExpression BuildContainsExpression<T>(Expression<Func<T, string>> stringProperty, ConstantExpression searchTermExpression)
    {
        return Expression.Call(stringProperty.Body, typeof(string).GetMethod("Contains"), searchTermExpression);
    }

}

This will allow you to write the following lambda:

[HttpGet]
public JsonResult searchForSwimmers(string q)
{
    //designed to return [{name='foo',id='bar'}]

    String[] QueryTerms = q.Split(' '); //all the search terms are sep. by " "
    var groupResults = _db.SwimGroups.Search(g => g.Name, queryTerms) 
                                     .OrderByDescending(g => g.Name.StartsWithAny(QueryTerms) ? 1 : 0)
                                     .ThenBy( g => g)
                                     .Select(g => new { name = g.Name, id = g.ID });

    return Json(groupResults,JsonRequestBehavior.AllowGet);
}    
Servy
  • 202,030
  • 26
  • 332
  • 449
NinjaNye
  • 7,046
  • 1
  • 32
  • 46
2

That's because neither your ContainsAny or StartsWithAny extension methods can be translated into SQL.

As your database table is small (as noted in comments), just resolve your query, by calling .ToList() before you do the Where and OrderBy.

Try this:

var groupResults = _db.SwimGroups
        .ToList() //evaluate the query, bring it into memory
        .Where(g => g.Name.ContainsAny(QueryTerms)) 
        .OrderByDescending(g => g.Name.StartsWithAny(QueryTerms) ? 1 : 0)
        .ThenBy( g => g)
        .Select(g => new { name = g.Name, id = g.ID });
Mathew Thompson
  • 55,877
  • 15
  • 127
  • 148
  • Isnt .ToArray() more efficient? And the List doesnt really give you anything over the array. – Robert J. Apr 25 '13 at 12:46
  • @RobertJ. `List` uses an `Array` internally. Whilst maybe more efficient, I doubt the difference would be anything to write home about. – Mathew Thompson Apr 25 '13 at 12:47
  • While you are probably right that it doesn' matter, why not use the more efficient way if it comes for free? – Robert J. Apr 25 '13 at 12:48
  • @RobertJ. I think they boil down to virtually the same code: http://stackoverflow.com/questions/1105990/is-it-better-to-call-tolist-or-toarray-in-linq-queries – Mathew Thompson Apr 25 '13 at 12:50
  • I replaced my code with that which you posted above, I continue to get the error. Here is the exception detail: http://pastebin.com/iBhD8qs6 (is it alright to post links such as this in SO?) – HennyH Apr 25 '13 at 12:50
  • @HennyH Ah your `ContainsAny` can't be translated either, you'll have to use good old `Contains`. If you post that method I could try and rewrite it? – Mathew Thompson Apr 25 '13 at 12:52
  • I added it to the description for you. – HennyH Apr 25 '13 at 12:54
  • From a comment in the article @mattytommo linked: "If the Count is known in advance, the performance is identical. However, if the Count isn't known in advance, the only difference between ToArray() and ToList() is that the former has to trim the excess, which involves copying the entire array, whereas the latter doesn't trim the excess, but uses an average of 25% more memory. This will only have implications if the data type is a large struct. Just food for thought" – Dan Esparza Apr 25 '13 at 12:59
  • @HennyH Ah, you wouldn't be able to achieve that in LINQ-To-Entities, check my updated edit, you'll have to resolve it before the `Where` – Mathew Thompson Apr 25 '13 at 13:01
  • @mattytommo Is it not possible / not worth the effort to write such a function which can be translated into SQL? – HennyH Apr 25 '13 at 13:02
  • @HennyH Yeah it's possible, if your database table has a **lot** of records, then it's probably worth the effort. Otherwise, probably not. Does your table have many records? – Mathew Thompson Apr 25 '13 at 13:04
  • @mattytommo No, not many at all! I would say at max 300 people would have records (name,groups,times,etc...). – HennyH Apr 25 '13 at 13:06
  • @HennyH Then probably not :) – Mathew Thompson Apr 25 '13 at 13:06
  • Writing your own extension method to allow linq to translate the code will mean you don't have to save all your records to memory each time the code is hit. This can become costly if you start including relationships with other records as the numbers soon multiply – NinjaNye Apr 28 '13 at 21:58