Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

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

0.00/5 (No votes)
30 Oct 2011 1  
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.

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.

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.

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.

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> ModelBinder in Global.asax, we can use the following Attribute class:

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

Index Modification:

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 has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here