生成传出URL时选择了意外的路由

hor*_*rgh 14 c# asp.net-mvc-routing asp.net-mvc-3

请考虑以下路线:

routes.MapRoute(
    "route1",
    "{controller}/{month}-{year}/{action}/{user}"
);
routes.MapRoute(
    "route2",
     "{controller}/{month}-{year}/{action}"
);
Run Code Online (Sandbox Code Playgroud)

以下测试:

测试1

[TestMethod]
public void Test1()
{
    RouteCollection routes = new RouteCollection();
    MvcApplication.RegisterRoutes(routes);
    RequestContext context = new RequestContext(CreateHttpContext(), 
                                                new RouteData());

    DateTime now = DateTime.Now;
    string result;

    context.RouteData.Values.Add("controller", "Home");
    context.RouteData.Values.Add("action", "Index");
    context.RouteData.Values.Add("user", "user1");
    result = UrlHelper.GenerateUrl(null, "Index", null,
                                    new RouteValueDictionary(
                                        new
                                        {
                                            month = now.Month,
                                            year = now.Year
                                        }),
                                    routes, context, true);
    //OK, result == /Home/10-2012/Index/user1
    Assert.AreEqual(string.Format("/Home/{0}-{1}/Index/user1", now.Month, now.Year), 
                    result);
}
Run Code Online (Sandbox Code Playgroud)

测试2

[TestMethod]
public void Test2()
{
    RouteCollection routes = new RouteCollection();
    MvcApplication.RegisterRoutes(routes);
    RequestContext context = new RequestContext(CreateHttpContext(), 
                                                new RouteData());

    DateTime now = DateTime.Now;
    string result;

    context.RouteData.Values.Add("controller", "Home");
    context.RouteData.Values.Add("action", "Index");
    context.RouteData.Values.Add("user", "user1");
    context.RouteData.Values.Add("month", now.Month + 1);
    context.RouteData.Values.Add("year", now.Year);
    result = UrlHelper.GenerateUrl(null, "Index", null,
                                    new RouteValueDictionary(
                                        new
                                        {
                                            month = now.Month,
                                            year = now.Year
                                        }),
                                    routes, context, true);
    //Error because result == /Home/10-2012/Index
    Assert.AreEqual(string.Format("/Home/{0}-{1}/Index/user1", now.Month, now.Year), 
    result);
}
Run Code Online (Sandbox Code Playgroud)

当我在请求上下文中已经有路由值并尝试使用UrlHelper生成传出URL时,此测试会模拟这种情况.

问题是(在测试2中给出),如果我有来自预期路线(这里route1)的所有段的值并尝试通过routeValues参数替换它们中的一些,则省略所需路线并使用下一个合适的路线.

因此,测试1运行良好,因为请求上下文已经具有路由1的5个段中的3个的值,并且缺失的两个段(即,yearmonth)的值通过该routeValues参数传递.

测试2具有请求上下文中所有5个段的值.我想用其他值替换其中一些(即月份和年份)routeValues.但是路线1似乎 不合适并且使用路线2.

为什么?我的路线有什么不对?

我是否希望在这种情况下手动清除请求上下文?

编辑

[TestMethod]
public void Test3()
{
    RouteCollection routes = new RouteCollection();
    MvcApplication.RegisterRoutes(routes);
    RequestContext context = new RequestContext(CreateHttpContext(), 
                                                new RouteData());

    DateTime now = DateTime.Now;
    string result;

    context.RouteData.Values.Add("controller", "Home");
    context.RouteData.Values.Add("action", "Index");
    context.RouteData.Values.Add("month", now.Month.ToString());
    context.RouteData.Values.Add("year", now.Year.ToString());
    result = UrlHelper.GenerateUrl(null, "Index", null,
                                    new RouteValueDictionary(
                                        new
                                        {
                                            month = now.Month + 1,
                                            year = now.Year + 1
                                        }),
                                    routes, context, true);
    Assert.AreEqual(string.Format("/Home/{0}-{1}/Index", now.Month + 1, now.Year + 1), 
                    result);
}
Run Code Online (Sandbox Code Playgroud)

这个测试使事情更加困惑.我在这里测试route2.它的工作原理!我在请求上下文中有所有4个段的值,传递其他值routeValues,并且生成的传出URL是正常的.

所以,问题在于route1.我错过了什么?

编辑

来自Sanderson S. Freeman A. - Pro ASP.NET MVC 3 Framework第三版:

路由系统按照它们添加到传递给RegisterRoutes方法的RouteCollection对象的顺序处理路由.检查每条路线是否匹配,这需要满足三个条件:

  1. 必须为URL模式中定义的每个段变量提供一个值.要查找每个段变量的值,路由系统首先查看我们提供值(使用匿名类型的属性),然后查看当前请求的变量值,最后查看路径中定义的默认值.
  2. 我们为段变量提供的值都不会与路径中定义的仅默认变量不一致.我在这些路线中没有默认值
  3. 所有段变量的值必须满足路径约束.我在这些路线中没有约束

所以,根据我在匿名类型中指定值的第一条规则,我没有默认值.当前请求的变量值 - 我想这是来自请求上下文的值.

route2的这些推理有什么问题,而它们适用于route1?

编辑

实际上,一切都不是来自单元测试,而是来自真正的mvc应用程序.在那里,我使用UrlHelper.Action方法(String,Object)来生成传出的URL.由于此方法用于布局视图(大多数视图的父视图),我已将其纳入我的扩展帮助方法(从视图中排除额外的逻辑),此扩展方法从请求中提取操作名称上下文,作为参数传递给它.我知道我可以通过请求上下文提取所有当前路由值并替换那些年份月份(或者我可以创建一个匿名路由值集合,包含上下文中的所有值),但我认为这是多余的,因为mvc会自动考虑请求上下文中包含的值.所以,我只提取了动作名称,因为没有动作名称没有UrlHelper.Action重载(或者我甚至喜欢"不指定"动作名称),并通过匿名路由值对象添加了新的月份和年份.

这是一种扩展方法:

public static MvcHtmlString GetPeriodLink(this HtmlHelper html, 
                                          RequestContext context, 
                                          DateTime date)
{
    UrlHelper urlHelper = new UrlHelper(context);
    return MvcHtmlString.Create(
              urlHelper.Action(
                 (string)context.RouteData.Values["action"], 
                 new { year = date.Year, month = date.Month }));
}
Run Code Online (Sandbox Code Playgroud)

正如我在上面的测试中描述的那样,它适用于较短的路径(当请求上下文仅包含控制器,年份和月份以及操作时),但是对于较长的路径失败(当请求上下文包含控制器,年份和月份,操作和用户时) ).


我发布了一个解决方法,用于使路由以我需要的方式工作.

虽然我确实很想知道,为什么我必须在这种情况下做出任何解决方法,这两条路线之间的关键区别是什么阻止route1了工作方式呢route2.


编辑

另一个评论.至于请求上下文中的值是类型的string,我决定尝试将它们设置为上下文作为字符串,以确保没有类型混淆(int vs string).我不明白,在这方面发生了什么变化,但有些路线开始正确生成.但并非所有......这使得意义不大.我在一个真实的应用程序中更改了这个,而不是测试,因为测试具有int上下文,而不是字符串.

好吧,我已经找到了使用route1的条件- 它仅在上下文中的month和的值year等于匿名对象中给出的值时使用.如果它们不同(在测试中这对于int和都是如此string),则使用route2.但为什么?

这证实了我在实际应用中的含义:

  1. string在上下文中,但int通过匿名对象提供s,它以某种方式混淆了mvc并且无法使用route1.
  2. 我改变int在匿名对象为s stringS,并且其中所述网址monthyear在上下文中是等于在匿名对象的那些,开始正确地产生; 而其他所有人都没有.

因此,我看到一个规则:匿名对象的属性应该是类型,string以匹配请求上下文中的路由值的类型.

但是这个规则似乎不是强制性的,因为在Test3中,我改变了类型(你现在可能会看到它)并且它仍然可以正常工作.MVC设法正确地转换类型.


最后,我找到了所有这些行为的解释.请看下面的答案.

hor*_*rgh 3

这是我用来让它工作的快速解决方法:

public static MvcHtmlString GetPeriodLink(this HtmlHelper html, 
                                          RequestContext context, 
                                          DateTime date)
{
    UrlHelper urlHelper = new UrlHelper(context);

    context.RouteData.Values["month"] = date.Month;
    context.RouteData.Values["year"] = date.Year;

    return MvcHtmlString.Create(
              urlHelper.Action(
                 (string)context.RouteData.Values["action"]));
}
Run Code Online (Sandbox Code Playgroud)

我只是从 中删除month和条目。yearcontext.RouteData.Values我只是替换了请求上下文中的month和条目。year如果从上下文中删除它们(就像我一开始所做的那样),它们对于在此之后调用的帮助器方法将不可用。

这种方法使我的扩展方法按照测试 1中描述的场景工作(请参阅问题)。


终于

仔细重读Sanderson S., Freeman A. - Pro ASP.NET MVC 3 Framework (第 3 版)我至少找到了所有这些东西的解释:

第 2 部分 ASP.NET MVC 详细信息

第 11 章 URL、路由和区域

生成传出 URL

理解段变量重用部分:

路由系统将仅重用 URL 模式中早于提供给 Html.ActionLink 方法的任何参数出现的段变量的值。

只要我的month-year段在之后满足并且我确实为和controller指定了值,则所有尾随段 ( , ) 都不会被重用。据我没有在匿名对象中指定它们,它们似乎不可用于该路线。因此,route1 无法匹配。monthyearactionuser

书中甚至有这样的警告:

处理这种行为的最好方法就是防止它发生。我们强烈建议您不要依赖此行为,并为 URL 模式中的所有段变量提供值。依赖这种行为不仅会使您的代码更难阅读,而且您最终会对用户发出请求的顺序做出假设,这最终会在您的应用程序进入维护时困扰您。

好吧,它咬了我)))

值得失去 100 个代表来记住(我什至会在这里再次重复)规则:路由系统将仅重用 URL 模式中早于所提供的任何参数出现的段变量的值。