Blog > ASP.NET MVC - Generic filtering based on expressions

ASP.NET MVC - Generic filtering based on expressions

by Daniel Palme 6 Comments

MagnifierRecently 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.

Views

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>.

The result

Result

Downloads

GenericSearch.7z

Tags: .NET, ASP, C#, MVC
Related posts:
 

New comment

:

:

:

:

 

Comments

#1
Nialo Nice piece off work. but there is a problem with datetime and int types when they arent nullable. crashes the CreatePredicateWithNullCheck function.

03/20/2012 by Nialo

#2
Daniel @Nialo:
Thanks for your hint. I have fixed this issue.

03/20/2012 by Daniel

#3
Jaros 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!

04/21/2012 by Jaros

#4
Daniel @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.

04/21/2012 by Daniel

#5
Jaros @Daniel

You'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.

04/23/2012 by Jaros

#6
jaros 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)

04/27/2012 by jaros