Click here to Skip to main content
15,867,308 members
Articles / Web Development / ASP.NET

ASP.NET MVC: Expression Trees as Parameter to Simplify Query

Rate me:
Please Sign up or sign in to vote.
4.82/5 (10 votes)
30 Oct 2011CPOL3 min read 31.3K   18   7
It shares an idea about simplifying query in ASP.NET MVC

Introduction

Because ASP.NET MVC introduces ModelBinder technology, we can receive Request data with strong typed parameter in Action, which is convenient for programming and improves our efficiency. We can take Expression Trees as parameter when inquiring Action and Simplify coding by creating Query Expression Tree with customized ModelBinder trends automatically.

At first, I will show the Model which will be used in this article.

C++
public class Employee {
    public int ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public bool Sex { get; set; }
    public DateTime? Birthday { get; set; }
    public string Remark { get; set; }
}

MVC Query and Shortcoming

The following shows an Action to inquire about Employee and it is often used in MVC.

C++
public ActionResult Index(string firstName, 
	string lastName, DateTime? birthday, bool? sex) {
    var employees = repository.Query();
    if (firstName.IsNotNullAndEmpty()) 
        employees = employees.Where(e => e.FirstName.Contains(firstName));
    if (firstName.IsNotNullAndEmpty()) 
        employees = employees.Where(e => e.LastName.Contains(lastName));
    if (birthday.HasValue) 
        employees = employees.Where(e => e.Birthday.Value.Date == birthday.Value.Date);
    if (sex.HasValue) 
        employees = employees.Where(e => e.Sex == sex);
    return View(employees);
} 

Because of MVC binding technology, we can get requested value through Action parameters easily, instead of Request[""].

In the above action, we can find that there are many ifs and it seems that the code is a little confusing. So we can simplify it as follows:

public ActionResult Index2(string firstName, string lastName, 
	DateTime? birthday, bool? sex) {
    var employees = repository.Query()
        .WhereIf(e => e.FirstName.Contains(firstName), firstName.IsNotNullAndEmpty())
        .WhereIf(e => e.LastName.Contains(lastName), lastName.IsNotNullAndEmpty())
        .WhereIf(e => e.Birthday.Value.Date == birthday.Value.Date, birthday.HasValue)
        .WhereIf(e => e.Sex == sex, sex.HasValue);
    return View("Index", employees);
}

Now, the code becomes clearer.

However, there are some shortcomings:

  1. There are several similar queries in web, such as Customer, Order, Product and so on. They have the same discipline: inquiring string fuzzily, inquiring date and time according to date (ignoring time), equal inquires for other types. Although the Action is inquired by different Model, the code is similar, but not repetitive and hard to reconstruct.
  2. Requirement is changed. If we want to add one query condition, we need to modify View and Action. If we want to add a parameter, we need to add Where or WhereIf. It is observed that we need to modify several parts if there are some changes.

In order to make up for the shortcomings, we can use Expression Trees as Action's parameter.

Use Expression <Func<T, bool>> as Action's Parameter

In the following code, I set Expression Trees as only parameter of Action (not considering paging and sorting) and collect all the query conditions to predicate parameter.

C++
public ActionResult Index3(Expression<Func<Employee, bool>> predicate) {
    var employees = repository.Query().Where(predicate);
    return View("Index", employees);
}    

All the queries (both Employee and Customer) need to use the above code. For other entity queries, we just need to change parameter's type, for example, change Customer as Expression<Func <Customer, bool>>.

However, there are errors if we run the code after modifying directly because DefaultModelBinder in MVC cannot bind Expression<Func <T, bool>>.

Therefore, we need to create a new ModelBinder.

Create QueryConditionExpressionModelBinder

We need a new ModelBinder to assign value to Expression<Func <T, bool>>, and name it as QueryConditionExpressionModelBinder.

QueryConditionExpressionModelBinder can generate Expression Trees automatically according to context. And we should pay attention to two points: typeof(T), the current Model type; value provided by Request, which can be gotten by ValueProvider.

The following code shows how to realize it roughly. It is just used to explain that this method is practicable.

C++
public class QueryConditionExpressionModelBinder : IModelBinder {
    public object BindModel(ControllerContext controllerContext, 
				ModelBindingContext bindingContext) {
        var modelType = GetModelTypeFromExpressionType(bindingContext.ModelType);
        if (modelType == null) return null;

        var body = default(Expression);
        var parameter = Expression.Parameter(modelType, modelType.Name);

        foreach (var property in modelType.GetProperties()){
            var queryValue = GetValueAndHandleModelState
		(property, bindingContext.ValueProvider, controllerContext.Controller);
            if (queryValue == null) continue;

            Expression proeprtyCondition = null;
            if (property.PropertyType == typeof (string)){
                if (!string.IsNullOrEmpty(queryValue as string)){
                    proeprtyCondition = parameter
                        .Property(property.Name)
                        .Call("Contains", Expression.Constant(queryValue));
                }
            }
            else if (property.PropertyType == typeof (DateTime?)){
                proeprtyCondition = parameter
                    .Property(property.Name)
                    .Property("Value")
                    .Property("Date")
                    .Equal(Expression.Constant(queryValue));
            }
            else{
                proeprtyCondition = parameter
                    .Property(property.Name)
                    .Equal(Expression.Constant(queryValue));
            }
            if (proeprtyCondition != null)
                body = body != null ? body.AndAlso(proeprtyCondition) : proeprtyCondition;
        }
        if (body == null) body = Expression.Constant(true);
        return body.ToLambda(parameter);
    }
    /// <summary>
    /// Get type of TXXX in Expression<func>>
    /// </func></summary>
    private Type GetModelTypeFromExpressionType(Type lambdaExpressionType) {

        if (lambdaExpressionType.GetGenericTypeDefinition() 
			!= typeof (Expression<>)) return null;

        var funcType = lambdaExpressionType.GetGenericArguments()[0];
        if (funcType.GetGenericTypeDefinition() != typeof (Func<,>)) return null;

        var funcTypeArgs = funcType.GetGenericArguments();
        if (funcTypeArgs[1] != typeof (bool)) return null;
        return funcTypeArgs[0];
    }
    /// <summary>
    /// Get query value of property and dispose Controller.NodelState
    /// </summary>
    private object GetValueAndHandleModelState(PropertyInfo property, 
		IValueProvider valueProvider, ControllerBase controller) {
        var result = valueProvider.GetValue(property.Name);
        if (result == null) return null;

        var modelState = new ModelState {Value = result};
        controller.ViewData.ModelState.Add(property.Name, modelState);

        object value = null;
        try{
            value = result.ConvertTo(property.PropertyType);
        }
        catch (Exception ex){
            modelState.Errors.Add(ex);
        }
        return value;
    }
} 

If we don't want to use set Expression<func>> ModelBinder in Global.asax, we can use the following Attribute class:

C++
public class QueryConditionBinderAttribute : CustomModelBinderAttribute {
    public override IModelBinder GetBinder() {
        return new QueryConditionExpressionModelBinder();
    }
}

Index Modification:

C++
public ActionResult Index3([QueryConditionBinder]Expression<func<employee> predicate) { //... }

After debugging, it shows to bind correctly.

Conclusion

The code in part is just used to prove that this method is practicable, so there is a large amount of hardcoding. Next, if I can find a more flexible method to write QueryConditionExpressionModelBinder to deal with complicated queries, I will show it to you. And hope that this article will be helpful for you.

If you want to learn more about Expression Trees, please visit:

My other article about ASP.NET recommendation:

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
New Zealand New Zealand
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
SuggestionSample demo using above concept Pin
satish.kotha21-Dec-12 9:28
professionalsatish.kotha21-Dec-12 9:28 
GeneralMy vote of 4 Pin
Monjurul Habib3-Nov-11 9:14
professionalMonjurul Habib3-Nov-11 9:14 
QuestionMy suggestion Pin
Rui Jarimba1-Nov-11 12:09
professionalRui Jarimba1-Nov-11 12:09 
GeneralMy vote of 3 Pin
User 482203331-Oct-11 18:32
User 482203331-Oct-11 18:32 
QuestionGot an example of what this looks like when called Pin
Sacha Barber31-Oct-11 5:16
Sacha Barber31-Oct-11 5:16 
QuestionGreat idea Pin
Nicolas Dorier28-Oct-11 23:33
professionalNicolas Dorier28-Oct-11 23:33 
GeneralMy vote of 3 Pin
Rupesh Kumar Tiwari28-Oct-11 9:22
Rupesh Kumar Tiwari28-Oct-11 9:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.