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.
Newer
Quick Random String
Newer
Quick Random String
browse with Pivot
Codility Nitrogenium Challenge
OS X Lock
HACT '13
Codility Challenges
Priority Queue
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)
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.