Hello World
Spiga

通过表达式树构造URL时忽略部分参数

2009-09-03 11:37 by 老赵, 5238 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的功能又前进了一小步。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

2 条回复

  1. deerchao
    *.*.*.*
    链接

    deerchao 2009-09-03 12:39:00

    <%= Url.ActionEx<HomeController>(c => c.List("", 1)) %>

  2. 老赵
    admin
    链接

    老赵 2009-09-03 12:45:00

    @deerchao
    如果Default是""那么自然可行,如果不是,就会出现在query string上了。
    实际情况下各种复杂情况都会出现,普通的ASP.NET MVC资料都只是随便一提。
    而且Post是个很特别的情况,它需要数据,但是又不能交给Routing处理,所以需要现在这么做。

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我