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>.
The result
Source Code
The latest source code can be found on GitHub.
Updates
16.05.2014: Migrated to MVC 5.
06.05.2018: Migrated to MVC Core.