ASP.NET MVC:使用表达式树作为参数来简化查询
本文分享了关于在ASP.NET MVC中简化查询的想法
引言
由于ASP.NET MVC引入了ModelBinder技术,我们可以在Action中通过强类型参数接收Request数据,这方便了编程并提高了我们的效率。当查询Action
时,我们可以使用表达式树作为参数,并通过使用自定义的ModelBinder
自动创建查询表达式树来Simplify
编码。
首先,我将展示本文中将使用的Model
。
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查询及其缺点
以下展示了一个用于查询Employee
的Action
,它经常在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);
}
由于MVC的绑定技术,我们可以通过Action
参数轻松获取请求的值,而不是使用Request[""]
。
在上面的action中,我们可以发现存在许多if
语句,这使得代码看起来有点混乱。因此,我们可以将其简化如下
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); }
现在,代码变得更清晰了。
然而,这仍然存在一些缺点
- Web中存在几种类似的查询,例如
Customer
、Order
、Product
等等。它们具有相同的规则:模糊查询字符串,根据日期查询日期和时间(忽略时间),对其他类型进行相等查询。尽管Action
是由不同的Model
查询的,但代码是相似的,但不是重复的,而且难以重构。 - 需求发生了变化。如果我们想添加一个查询条件,我们需要修改
View
和Action
。如果我们想添加一个参数,我们需要添加Where
或WhereIf
。我们观察到,如果发生一些变化,我们需要修改几个部分。
为了弥补这些缺点
,我们可以使用表达式树作为Action
的参数。
使用Expression <Func<T, bool>>作为Action的参数
在以下代码中,我将表达式树设置为Action
的唯一参数(不考虑分页和排序),并将所有查询条件收集到谓词参数中。
public ActionResult Index3(Expression<Func<Employee, bool>> predicate) {
var employees = repository.Query().Where(predicate);
return View("Index", employees);
}
所有查询(包括Employee
和Customer
)都需要使用上述代码。对于其他实体查询,我们只需要更改参数的类型,例如,将Customer
更改为Expression<Func <Customer, bool>>
。
但是,如果我们直接修改后运行代码,则会出现错误,因为MVC中的DefaultModelBinder
无法绑定Expression<Func <T, bool>>
。
因此,我们需要创建一个新的ModelBinder
。
创建QueryConditionExpressionModelBinder
我们需要一个新的ModelBinder
来为Expression<Func <T, bool>>
赋值,并将其命名为QueryConditionExpressionModelBinder
。
QueryConditionExpressionModelBinder
可以根据上下文自动生成表达式树。我们应该注意两点:typeof(T)
,当前的Model
类型;以及由Request
提供的值,可以通过ValueProvider
获得。
以下代码大致展示了如何实现它。它仅用于解释此方法是可行的。
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;
}
}
如果我们不想使用设置Expression
ModelBinder
,我们可以使用以下Attribute
类
public class QueryConditionBinderAttribute : CustomModelBinderAttribute {
public override IModelBinder GetBinder() {
return new QueryConditionExpressionModelBinder();
}
}
索引修改
public ActionResult Index3([QueryConditionBinder]Expression<func<employee> predicate) { //... }
调试后,它显示绑定正确。
结论
该部分的代码仅用于证明此方法是可行的,因此存在大量的硬编码。接下来,如果我能找到一种更灵活的方法来编写QueryConditionExpressionModelBinder
来处理复杂的查询,我将向您展示它。希望这篇文章对您有所帮助。
如果您想了解更多关于表达式树的信息,请访问
我关于ASP.NET的另一篇文章推荐