ASP.NET MVC - Generic filtering based on expressions
Recently I was asked to implement a reusable filtering mechanism in an ASP.NET MVC application. To be more concrete: A website shows a grid containing arbitrary data. The user should be able to enter a filter for each grid column.
The filters should be generated based on the type of the displayed objects. With that functionality, it is possible to filter every grid in the application with very little effort. Moreover I added a possibility to add custom search criteria.
Initial situation
The data layer consists of a single class called Repository. The repository has a method which returns an IQueryable:
public IQueryable<SomeClass> GetQuery()
Typically you will query a database to get the data, I just use a static collection of dummy data for simplicity.
The controller retrieves the data from the repository and the view renders the entries by using a WebGrid (nice introduction).
The tasks
To add the generic filtering the following tasks have to be accomplished:
- Definition of search criteria
- Generation of a search criteria collection based on the type of the desired object (and/or some custom criteria)
- Rendering of the search criteria
- Possiblity to apply the search criteria to an IQueryable
Definition of search criteria
Search criteria are based on the abstract class AbstractSearch this class has two important methods:
internal IQueryable<T> ApplyToQuery<T>(IQueryable<T> query)
{
var arg = Expression.Parameter(typeof(T), "p");
var property = this.GetPropertyAccess(arg);
Expression searchExpression = this.BuildExpression(property);
if (searchExpression == null)
{
return query;
}
else
{
var predicate = CreatePredicateWithNullCheck<T>(searchExpression, arg);
return query.Where(predicate);
}
}
protected abstract Expression BuildExpression(MemberExpression property);
Since the search criteria should work for arbitrary types, I used expressions for the search criteria logic.
Concrete search criteria have to implement the BuildExpression method. This method returns the expression which is evaluated to determine whether the criteria matches or not. For example the following criteria checks whether an integer property matches a given number:
public class NumericSearch : AbstractSearch
{
public int? SearchTerm { get; set; }
protected override Expression BuildExpression(MemberExpression property)
{
if (!this.SearchTerm.HasValue)
{
return null;
}
return Expression.Equal(property, Expression.Constant(this.SearchTerm.Value));
}
The base class takes care of creating the MemberExpression and also adds null checks to avoid null reference exceptions.
Generation of a search criteria collection based on the type of the desired object (and/or some custom criteria)
The search criteria should be created automatically for any type. Currently the following search criteria exist:
- DateTime/DateTime? (<, <=, ==, >, >=, >, InRange)
- int/int? (<, <=, ==, >, >=, >)
- string (Contains, Equals)
To determine the search criteria for a type the following method can be used:
public static ICollection<AbstractSearch> GetDefaultSearchCriterias(this Type type)
{
var properties = type.GetProperties()
.Where(p => p.CanRead && p.CanWrite)
.OrderBy(p => p.Name);
var searchCriterias = properties
.Select(p => CreateSearchCriteria(p.PropertyType, p.Name))
.Where(s => s != null)
.ToList();
return searchCriterias;
}
Reflection is used to determine all properties of the given class. For all supported property types a search criteria is added to a list. Custom search criteria can be added to that list.
Rendering of the search criteria
The controller in the ASP.NET MVC application is rather simple, the ActionMethod basically looks like this:
public ActionResult Index()
{
var model = new IndexModel()
{
Data = this.repository.GetQuery().ToArray(),
SearchCriteria = typeof(GenericSearch.Data.SomeClass).GetDefaultSearchCriterias()
};
return View(model);
}
By creating the following view templates also the main view can be kept quite simple.
The view again is very simple:
@model IndexModel
@using (Html.BeginForm())
{
@Html.EditorFor(m => m.SearchCriteria)
<br />
<input type="submit" name="default" value="Filter" />
<br /><br />
@(new WebGrid(this.Model.Data).GetHtml())
}
Possiblity to apply the search criteria to an IQueryable
The controller method which receives and applies the filters looks like this:
[HttpPost]
public ActionResult Index(ICollection<AbstractSearch> searchCriteria)
{
var model = new IndexModel()
{
Data = this.repository.GetQuery().ApplySearchCriterias(searchCriteria).ToArray(),
SearchCriteria = searchCriteria
};
return View(model);
}
One thing is worth to be mentioned. The method takes a collection of AbstractSearch. To create concrete search criteria, a custom model binder is used. The concrete class name is supplied in a hidden field.
Finally the ApplySearchCriterias extension is used to apply the search criteria to an IQueryable<T>.


New comment
Comments
Nialo
03/20/2012
Nice piece off work. but there is a problem with datetime and int types when they arent nullable. crashes the CreatePredicateWithNullCheck function.Daniel
03/20/2012
@Nialo:Thanks for your hint. I have fixed this issue.
Jaros
04/21/2012
Thanks for this blog, a bit short for what you got going on in code. But then again it's a blog and not a full fledged tutorial.Excellent coding skills I must say.
Kudos!
Daniel
04/21/2012
@Jaros:Thanks for your comment.
It always difficult to get a good balance between explaining every single line of code and presenting the high level concepts.
Because my time is limited I decided to show the main ideas and everybody who is interested in the details, can have a look at the code.
Jaros
04/23/2012
@DanielYou're right, I was merely suggesting that with the things that you have going on, like custom modelbinding, using the MVC Templating mechanism and building Expressions runtime, it could be worthwhile to make it a tutorial.
I agree that the code is written in such a way that you can easily follow what's going on.
jaros
04/27/2012
I extended your example to connect to the ModelMetadata in MVC.I thought I'd share it
Here is the code: in SearchExtensions.cs
public static ICollection<AbstractSearch> GetModelMetadataSearchCriterias(this Type type,Func<ModelMetadata,bool> modelCriteria )
{
var properties = type.GetProperties()
.OrderBy(p => p.Name);
var searchCriteria =
properties.Where(m => modelCriteria(GetModelMetadataForProperty(type, m.Name))).Select(p => CreateSearchCriteria(p.PropertyType, p.Name)).ToList();
return searchCriteria;
}
private static ModelMetadata GetModelMetadataForProperty(Type type, string propertyname)
{
ModelMetadataProvider modelMetadataProvider = ModelMetadataProviders.Current;
return modelMetadataProvider.GetMetadataForProperty(null, type, propertyname);
}
And in the Controller you use it like this:
SearchCriteriaNested = typeof(GenericSearch.Data.SomeClass)
.GetModelMetadataSearchCriterias((j) => j.ShowForEdit)
.AddCustomSearchCriteria<GenericSearch.Data.SomeClass>(s => s.SomeNestedClass.Id)