ASP.NET MVC 引入了 ModelBinder 技術,讓我們可以在 Action 中以強類型參數的形式接收 Request 中的數據,極大的方便了我們的編程,提高了生產力。在查詢 Action 中,我們可以將?Expression Trees?用作參數,通過自定義的 ModelBinder 動態自動構建查詢表達式樹,進一步發揮 MVC 的威力,簡化編碼工作。
?
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["XXXX"] 的方式。
?
仔細觀察,會發現上面這個 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); }
代碼相清晰了許多,我之前的幾個 MVC 項目中也是這樣處理的。
但時間一長,我逐步也發現了這種方式一些不足之處:
- 首先,網站中有很多類似的查詢,如Customer、Order、Product 等等。而且大致也有點規律:字符串的一般模糊查詢,時間日期類的一般按日期查詢(忽略時間),其它類型則相等查詢。不同 Model 查詢的 Action 編碼總有八、九分相似,但又不是簡單的重復,卻又難以重構。
- 需求變動,如增加一個查詢條件,修改 View 是必須的,但也要修改 Action,增加一個參數,還要加一行 Where 或 WhereIf。簡單變動卻多處修改,煩人啊,而且這種需求變動又是比較頻繁的,尤其是在項目初期。若能只修改 View 而不修改 Action 就爽了。
思考后,我決定使用?Expression Trees?作為查詢 Action的參數來彌補這些不足。
使用 Expression<Func<T, bool>> 作為 Action 的參數
public ActionResult Index3(Expression<Func<Employee, bool>> predicate) {var employees = repository.Query().Where(predicate);return View("Index", employees); }
將?Expression Trees?作為 Action 的唯一的參數(暫不考慮分頁、排序等),將所有的查詢條件都統一匯集至? predicate 參數。
所有的查詢(不管是 Employee 還是 Customer)都使用如上代碼。其它實體查詢只需修改參數的類型,如 Customer 查詢改為 Expression<Func<Customer, bool>> 。
如上修改代碼后,直接運行會報錯,因為 MVC 中默認的數據綁定器 DefaultModelBinder 不能正確綁定 Expression<Func<T, bool>> 類型的參數。
我們要新創一個新的 ModelBinder。
創建 QueryConditionExpressionModelBinder
?需要一個新的 ModelBinder 來為 Expression<Func<T, bool>> 類型的參數賦值,且命名為 QueryConditionExpressionModelBinder。
QueryConditionExpressionModelBinder 要根據上下文來自動生成查詢的?Expression Trees。主要關注的上下文有兩點:首先是當前 Model 的類型,即 typeof(T);其次是 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>/// 獲取 Expression<Func<TXXX, bool>> 中 TXXX 的類型/// </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>/// 獲取屬性的查詢值并處理 Controller.ModelState /// </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;} }
如果不想在 Global.asax 文件中設置 Expression<Func<T, bool>> 的 ModelBinder, 可以借助用下面這個 Attribute 類:
public class QueryConditionBinderAttribute : CustomModelBinderAttribute {public override IModelBinder GetBinder() {return new QueryConditionExpressionModelBinder();} }
Index3 簡單修改如下:
public ActionResult Index3([QueryConditionBinder]Expression<Func<Employee, bool>> predicate) { //... }
?
?
?