ASP.NET MVC - Generic filtering based on expressions

 
2/18/2012
.NET, ASP.NET, C#, MVC
25 Comments

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.

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

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.

Downloads

Feedly Feedly Tweet


Related posts


Comments


Daniel

Daniel

4/23/2015

@Habib, Adi, Henry: I created a new issue for your feature request: https://github.com/danielpalme/GenericSearch/issues/3 I'm very busy at the moment. I already started implementing support for complex solutions. Hopefully I can finish this within the next two weeks. Stay tuned!


Henry Brenes

Henry Brenes

4/23/2015

Hi Daniel, Thanks for this post. Very usefull. These days I've been working on a search filter like this, so this is an excellent material that sheds light on my work. A desirable feature that would make the code even more wonderful than it already is the ability to specify filters on properties of nested collections on multiple levels (one or more levels of nesting ) . Pseudo-Linq example: entities.where(w => w.entityCollectionProperty.Any(a => a.innerEntityCollectionProperty.Any(b => b.SomeOtherProp == "abc")) Regards


Adi Silagy

Adi Silagy

4/9/2015

Nice coding I must say! This was very helpful for me in several projects, i extended some of your code to get a some specific functionality. There is a something i couldn't do and i am lost of track even on where to start. I have a class [User] This User has a collection of [Roles], which this class has the following properties: RoleID RoleName I would like to allow filtering based on a specific role (in the UI i will present a drop-down with list of roles, and the selected one will filter the list of users who have this role). Any help will be appreciated Thanks!


Habib

Habib

4/9/2015

Thanks for this blog, How can we customise this project to search for an object that have not only a related object but also a list of children object . this object which we are looking for like this : public class SomeClass { public DateTime Date { get; set; } public DateTime? DateNullable { get; set; } [Display(Name = "Integer with label")] public int Integer { get; set; } public int? IntegerNullable { get; set; } public MyEnum MyEnum { get; set; } public string Text { get; set; } public SomeNestedClass Nested { get; set; } public List<SomeListNestedClass> ListNesteds { get; set; } }


Rog

Rog

6/9/2014

Thanks Dan, that's great. I'll try and work out what's going on and how the various bits get invoked! Cheers Rog


Daniel

Daniel

6/6/2014

@Rog: I added a demo for paging/sorting. Please have look at the latest source code.


Rog

Rog

6/4/2014

Dan, I'm new to MVC, although I've done a bit of ASP.NET before. I was initially looking at how to add sorting and pagination to an MVC view, following this example : http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/sorting-filtering-and-paging-with-the-entity-framework-in-an-asp-net-mvc-application Then I discovered your generic search facility, which is fantastic...but I'm having trouble trying to integrate your search methodology with the sort/pagination of the other site. In a nutshell, I've got my controller looking like this :- public ViewResult Index(string sortOrder, int? page, ICollection<AbstractSearch> searchCriteria) but when the form button is pressed, the page & sortorder are always null, and I'm not certain how to populate the 3 arguments from the view. Any help would be greatly appreciated. Basically I'm just trying to follow the URL listed, but using your generic search stuff Thanks Rog


Daniel

Daniel

5/16/2014

@Mathie: I migrated the solution to MVC 5 and also included all the new features. The code is also on Github now. The old solution for MVC 3 is available as a separate branch in the repository.


Daniel

Daniel

5/15/2014

@Mathie: Yes I can to that. Hopefully I will find some time to do that next week.


Mathie

Mathie

5/15/2014

Thank you, very usefull. Could you publish the code for changing the labels where you enter your search criteria via DataAnnotations?


Daniel

Daniel

3/28/2014

@lluthus: I just dropped you an email.


lluthus

lluthus

3/28/2014

Can you post an example ?


Daniel

Daniel

3/28/2014

@lluthus: You want to change the labels where you enter your search criteria? Yes you can use annotations. E.g. [System.ComponentModel.DataAnnotations.Display(Name = "Some other name")] But you also have to change the implementation of the property 'LabelText' in the class 'AbstractSearch'. You should not evaluate the property name, but you have to check the annotation. You need some reflection code to check the corresponding attributes of the property.


lluthus

lluthus

3/28/2014

It is very important for me, i would like add [Required] too someClass, But i can't :( Please help me ;)


lluthu

lluthu

3/28/2014

Thank you Daniel you are very kind! But, I do not want to change the labels of the columns in the table, i'm using jquery datatables for this. I would like to display custom labels on the filters I would like to apply the DataAnnotations on filters It is possible ? Thank you an have nice day from Milan!


Daniel

Daniel

3/27/2014

@lluthus: Please see: http://stackoverflow.com/questions/5250735/how-can-i-use-displayname-data-annotations-for-column-headers-in-webgrid


lluthus

lluthus

3/27/2014

Hi i love this Filter, but i need change only the display name of label filter, it is possible ? In the SomeClass, DataAnnotation, does't work: [Display(Nome="OtherName")] public int Integer { get; set; } Thak you in advantage


Phil

Phil

7/24/2013

Hi. Very useful example! Thank you. I have to say, that i created something similar before for an Custom Unique Validation Attribute and some other kind of declarative approaches with PostSharp. Therefore I defined my IRepository interface like this: public interface IRepository<T, in TUser> : ITransactional, IDisposable { IEnumerable<T> Get(Expression<Func<T, bool>> filter = null, LambdaExpression lambda = null, Func<IQueryable<T>, IOrderedQueryable<T>> orderBy = null, string includeProperties = ""); IEnumerable<T> GetByFetchMode(FetchMode mode); T GetByID(int id); void Insert(T dbObject); .... } and use this kind of extensions: public static LambdaExpression BuildLambdaExpressionForEquality(this Type classType, string propertyName, object value) { ParameterExpression predicateParam = Expression.Parameter(classType, "x"); Expression left = Expression.Property(predicateParam, classType.GetProperty(propertyName)); Expression right = Expression.Constant(value, value.GetType()); Expression equality = Expression.Equal(left, right); Type predicateType = typeof(Func<,>).MakeGenericType(classType, typeof(bool)); LambdaExpression lambda = Expression.Lambda(predicateType, equality, predicateParam); return lambda; }


Anavrin72

Anavrin72

6/3/2013

You can also try Sprint.Filter https://github.com/artem-sedykh/Sprint.Filter


jaros

jaros

4/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)


Jaros

Jaros

4/23/2012

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


Daniel

Daniel

4/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

Jaros

4/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

Daniel

3/20/2012

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


Nialo

Nialo

3/20/2012

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