Register | Login

Stacking Code

public interface IBlog { string Dump(Stream consciousness); }

Autocomplete, IQueryable<T> and Linq Expressions

Friday, 14 October, 2011 @ 8:31 PM < Adam Boddington
Tags: ASP.NET MVC, jQuery, Linq

Wow, here's another post, this time from March. Looks like I finished it but never hit publish.

The challenge was to create an autocomplete text box on an ASP.NET MVC view. Straight forward enough, the back-end looked something like this (class and property names have been changed).

[HttpPost]
public JsonResult WidgetAutoComplete(string term)
{
    var widgets = WidgetService.GetWidgets()
        .Where(widget => widget.Name.ToLower().Contains(term.ToLower()))
        .OrderBy(widget => widget.Tag)
        // Reformat to something jQuery Autocomplete understands.
        .Select(widget => new { id = widget.Id, label = widget.Tag + " " + widget.Name });

    return Json(widgets);
}

My service is returning an IQueryable<Widget> for me to play with. It's awesome the NHibernate Linq provider can interpret ToLower and Contains into LOWER and LIKE, but I guess other ORMs can do that too. A quick check of the SQL shows everything is working okay.

Next iteration the client wanted to be able to search on Widget.Tag as well as Widget.Name.

[HttpPost]
public JsonResult WidgetAutoComplete(string term)
{
    var widgets = WidgetService.GetWidgets()
        .Where(widget => widget.Name.ToLower().Contains(term.ToLower()) || widget.Tag.ToLower().Contains(term.ToLower()))
        .OrderBy(widget => widget.Tag)
        // Reformat to something jQuery Autocomplete understands.
        .Select(widget => new { id = widget.Id, label = widget.Tag + " " + widget.Name });

    return Json(widgets);
}

The Where got a bit bigger, but nothing else.

Next iteration the client wanted to be able to type more than one word, and have the words match on any part of the widget tag or name. The words can be in any order. The result set must have all the words present in either the tag or name.

[HttpPost]
public JsonResult WidgetAutoComplete(string term)
{
    string[] terms = term.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

    IQueryable<Widget> widgets = WidgetService.GetWidgets();

    foreach (string t in terms)
        widgets = widgets
            .Where(widget => widget.Name.ToLower().Contains(t.ToLower()) || widget.Tag.ToLower().Contains(t.ToLower()));

    var results = widgets
        .OrderBy(widget => widget.Tag)
        // Reformat to something jQuery Autocomplete understands.
        .Select(widget => new { id = widget.Id, label = widget.Tag + " " + widget.Name });

    return Json(results);
}

Okay, so getting hard to read now.

The next iteration the client loves the functionality and asks for it to become standard practice for all autocomplete text boxes in the application. In other words, this process will happen for other classes with different properties. Not wanting to repeat the same hard to read code in all the autocomplete actions, thus began a foray into System.Linq.Expressions to try to refactor the above into something more like this...

[HttpPost]
public JsonResult WidgetAutoComplete(string term)
{
    string[] terms = term.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);

    var widgets = WidgetService.GetWidgets()
        .MatchingTerms(terms, widget => widget.Tag, widget => widget.Name)
        .OrderBy(widget => widget.Tag)
        // Reformat to something jQuery Autocomplete understands.
        .Select(widget => new { id = widget.Id, label = widget.Tag + " " + widget.Name });

    return Json(widgets);
}

Much, much cleaner, but how does it work?

public static class Extensions
{
    private static readonly MethodInfo CONTAINS_METHOD = typeof(string).GetMethod("Contains", new[] { typeof(string) });
    private static readonly MethodInfo TO_LOWER_METHOD = typeof(string).GetMethod("ToLower", new Type[] { });

    public static IQueryable<T> MatchingTerms<T>(this IQueryable<T> source, string[] terms, params Expression<Func<T, string>>[] properties)
    {
        foreach (string term in terms)
        {
            Expression<Func<T, bool>> predicate = null;

            foreach (var property in properties)
            {
                Expression propertyExpression = property.Body;
                ParameterExpression genericParameter = property.Parameters[0];

                MethodCallExpression toLowerExpression = Expression.Call(propertyExpression, TO_LOWER_METHOD);

                ConstantExpression termConstant = Expression.Constant(term, typeof(string));
                MethodCallExpression containsExpression = Expression.Call(toLowerExpression, CONTAINS_METHOD, termConstant);

                Expression<Func<T, bool>> partialPredicate = Expression.Lambda<Func<T, bool>>(containsExpression, genericParameter);

                if (predicate == null)
                    predicate = partialPredicate;
                else
                    predicate = Expression.Lambda<Func<T, bool>>(Expression.OrElse(predicate.Body, partialPredicate.Body), genericParameter);
            }

            if (predicate != null)
                source = source.Where(predicate);
        }

        return source;
    }
}

Each Expression<Func<T, string>> is teased apart to get the body expression onto which the extra method calls will be tacked on. After ToLower and Contains have been added, the expression is repackaged back up as an Expression<Func<T, bool>> using the original generic parameter.

If there's more than one property, the partial predicates are chained together using Expression.OrElse (equivalent to ||). The generic parameter from the latest partial predicate is used here, which might be odd, except it's really the same generic parameter for all the Expression<Func<T, string>> expressions, being an instance of T from IQueryable<T>.

The end result is an expression tree equivalent to the one hardcoded earlier, allowing the NHibernate Linq provider to proceed as normal. This should work with any Linq provider, however, as long as they can handle the same string functions.

I think I need to lie down.

There are 1 comments.


Comments

Jules Bartow wrote on Monday, 25 June, 2012 @ 2:03 AM

Sweet!

This saved me a lot of time writing the code for predicates and makes the search work like Google. You're awesome.

Leave a Comment

Please register or login to leave a comment.


Older
Database Migrations with PowerShell

Newer
Quick Random String

Older
Database Migrations with PowerShell

Newer
Quick Random String

browse with Pivot


About


Projects

Building Neno


RSS
Recent Posts

Codility Nitrogenium Challenge
OS X Lock
HACT '13
Codility Challenges
Priority Queue


Tags

Architecture (13)
ASP.NET (2)
ASP.NET MVC (13)
Brisbane Flood (1)
Building Neno (38)
C# (4)
Challenges (3)
Collections (1)
Communicator (1)
Concurrency Control (2)
Configuration (1)
CSS (5)
DataAnnotations (2)
Database (1)
DotNetOpenAuth (2)
Entity Framework (1)
FluentNHibernate (2)
Inversion of Control (5)
JavaScript (1)
jQuery (4)
Kata (2)
Linq (7)
Markdown (4)
Mercurial (5)
NHibernate (20)
Ninject (2)
OpenID (3)
OS X (1)
Pivot (6)
PowerShell (8)
Prettify (2)
RSS (1)
Spring (3)
SQL Server (5)
T-SQL (2)
Validation (2)
Vim (1)
Visual Studio (2)
Windows Forms (3)
Windows Service (1)


Archives


Powered by Neno, ASP.NET MVC, NHibernate, and small furry mammals. Copyright 2010 - 2011 Adam Boddington.
Version 1.0 Alpha (d9e7e4b68c07), Build Date Sunday, 30 January, 2011 @ 11:37 AM