65.9K
CodeProject 正在变化。 阅读更多。
Home

ASP.NET MVC:使用表达式树作为参数来简化查询

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.82/5 (7投票s)

2011年10月28日

CPOL

3分钟阅读

viewsIcon

32767

本文分享了关于在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查询及其缺点

以下展示了一个用于查询EmployeeAction,它经常在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);
}

现在,代码变得更清晰了。

然而,这仍然存在一些缺点

  1. Web中存在几种类似的查询,例如CustomerOrderProduct等等。它们具有相同的规则:模糊查询字符串,根据日期查询日期和时间(忽略时间),对其他类型进行相等查询。尽管Action是由不同的Model查询的,但代码是相似的,但不是重复的,而且难以重构。
  2. 需求发生了变化。如果我们想添加一个查询条件,我们需要修改ViewAction。如果我们想添加一个参数,我们需要添加WhereWhereIf。我们观察到,如果发生一些变化,我们需要修改几个部分。

为了弥补这些缺点,我们可以使用表达式树作为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);
}    

所有查询(包括EmployeeCustomer)都需要使用上述代码。对于其他实体查询,我们只需要更改参数的类型,例如,将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>在Global.asax中设置ModelBinder,我们可以使用以下Attribute

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

索引修改

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

调试后,它显示绑定正确。

结论

该部分的代码仅用于证明此方法是可行的,因此存在大量的硬编码。接下来,如果我能找到一种更灵活的方法来编写QueryConditionExpressionModelBinder来处理复杂的查询,我将向您展示它。希望这篇文章对您有所帮助。

如果您想了解更多关于表达式树的信息,请访问

我关于ASP.NET的另一篇文章推荐

© . All rights reserved.