通过表达式树构造URL时忽略部分参数
2009-09-03 11:37 by 老赵, 5273 visits您的使用ASP.NET MVC的时候,一定遇到过使用Post接受数据的Action方法。例如:
public class HomeController : Controller { [AcceptVerbs(HttpVerbs.Post)] public ViewResult List(string keywords, int page) { ... } }
于是乎,客户端只要像Home/List这样的URL中Post数据,这个Controller便可以从请求的Body中获得keyword和page的值。为了实现这个功能,我们必须在客户端准备一个form,把它的Action——也就是Post的目标URL写为Home/List。但是这个URL改怎么生成呢?按照传统的做法,我们会使用表达式树来构造这个URL:
<%= Url.ActionEx<HomeController>(c => c.List("hello", 3)) %>
但是您会发现,上面这条语句最终生成的URL是:
Home/List?keywords=hello&page=3
这是因为ASP.NET Routing在处理配置规则中没有标明的Route Values时,会将它们作为Query String拼接在URL后面。这也是可以预料到的,因为作为Form的URL,我们又如何明确指定一个参数的值呢?无论指定什么值都是不合适的,我们必须将它们忽略掉——或者说,我们需要找一种可以表示“任意”参数的方式。
接下来的做法还是接着上次的结果继续改进。您会发现这种做法有明显的Moq框架的影子,因为我们要使用这样的方式来表示参数的忽略:
<%= Url.ActionEx<HomeController>(c => c.List(It.IsAny<string>(), It.IsAny<int>())) %>
这需要我们准备一个简单的It.IsAny方法的“结构”:
public static class It { public static T IsAny<T>() { string message = "Use for expression construction only, " + "please DO NOT execute directly"; throw new InvalidOperationException(message); } }
这个方法不是用来直接调用的,它只是作为表达式树的一部分存在——这也再次说明,表达式树的构造,并不意味着一定执行。表达式树是一种表示方式,用来说明我们的“意图”,仅此而已。
在原先的代码中,我们是这样向一个RouteValueDictionary里填充数据的:
private static void AddParameterValues(RouteValueDictionary rvd, MethodCallExpression call) { ParameterInfo[] parameters = call.Method.GetParameters(); for (int i = 0; i < parameters.Length; i++) { rvd.Add(parameters[i].Name, Eval(call.Arguments[i])); } }
如今,call.Arguments[i]可能是一个It.Any<…>()表达式,它不能直接用于求值(Eval)。因此,我们要将代码修改为以下这样:
private static void AddParameterValues(RouteValueDictionary rvd, MethodCallExpression call) { ParameterInfo[] parameters = call.Method.GetParameters(); for (int i = 0; i < parameters.Length; i++) { var arg = call.Arguments[i]; if (!IsParameterShouldBeIgnored(arg)) { rvd.Add(parameters[i].Name, Eval(arg)); } } } private static bool IsParameterShouldBeIgnored(Expression arg) { var call = arg as MethodCallExpression; if (call == null) return false; if (call.Method.DeclaringType != typeof(It)) return false; if (call.Method.Name != "IsAny") return false; return true; }
我们在求值之前,需要判断这个表达式是否是It.IsAny方法的调用。如果不是,才将其加入RouteValueDictionary中。就这样,修改结束了,总共也就10多行代码的改动而已。
为了检验我们的成果,最好的方法进行单元测试。首先,我们准备一个测试用的Controller类和Action方法:
private class TestController : Controller { public ActionResult Index(string s, int i) { return null; } }
然后检查在普通情况下,所有的Route Value都被正常捕获到:
[Fact] public void Get_Route_Values_With_Arguments() { var routeValues = RouteExpression.GetRouteValues<TestController>( c => c.Index("abc", 5)); Assert.Equal("Test", routeValues["controller"]); Assert.Equal("Index", routeValues["action"]); Assert.Equal("abc", routeValues["s"]); Assert.Equal(5, routeValues["i"]); }
以及,如果我们想要忽略到一个参数时,它就不会出现在RouteValueDictionary中:
[Fact] public void Get_Route_Values_With_Ignored_Arguements() { var routeValues = RouteExpression.GetRouteValues<TestController>( c => c.Index("abc", It.IsAny<int>())); Assert.Equal("Test", routeValues["controller"]); Assert.Equal("Index", routeValues["action"]); Assert.Equal("abc", routeValues["s"]); Assert.False(routeValues.ContainsKey("i"), "This arg should be ignored!"); }
就这样,我们通过表达式树生成URL的功能又前进了一小步。
<%= Url.ActionEx<HomeController>(c => c.List("", 1)) %>