Daniel Palme

Daniel Palme

.NET consultant from Germany.

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

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.

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.

Downloads

GenericSearch.7z


Subscribe to RSS Feed

Tags: .NET, ASP, C#, MVC
 

Related posts

 

New comment

:

:

:

:

 

Comments

#1
Nialo

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.
 
#2
Daniel

Daniel

03/20/2012

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

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!
 
#4
Daniel

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.
 
#5
Jaros

Jaros

04/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.
 
#6
jaros

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)
 
#7
Anavrin72

Anavrin72

06/03/2013

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

Phil

07/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;
}
 
#9
lluthus

lluthus

03/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
 
#10
Daniel

Daniel

03/27/2014

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

lluthu

03/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!
 
#12
lluthus

lluthus

03/28/2014

It is very important for me, i would like add [Required] too someClass,

But i can't :(

Please help me ;)
 
#13
Daniel

Daniel

03/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.
 
#14
lluthus

lluthus

03/28/2014

Can you post an example ?
 
#15
Daniel

Daniel

03/28/2014

@lluthus:
I just dropped you an email.
 
#16
Mathie

Mathie

05/15/2014

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

Daniel

05/15/2014

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

Daniel

05/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.
 
#19
Rog

Rog

06/04/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
 
#20
Daniel

Daniel

06/06/2014

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

Rog

06/09/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
 
#22
Habib

Habib

04/09/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; }
}
 
#23
Adi Silagy

Adi Silagy

04/09/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!
 
#24
Henry Brenes

Henry Brenes

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

 
#25
Daniel

Daniel

04/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!