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 if
s 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:
- 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.
- 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);
}
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];
}
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: