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

时间:2012-10-26 06:29:49

标签: c# asp.net-mvc-3 asp.net-mvc-routing

请考虑以下路线:

routes.MapRoute(
    "route1",
    "{controller}/{month}-{year}/{action}/{user}"
);
routes.MapRoute(
    "route2",
     "{controller}/{month}-{year}/{action}"
);

以下测试:

TEST 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);
}

TEST 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);
}

此测试模拟了我在请求上下文中已有路由值并尝试使用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);
}

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

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

修改

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

  

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

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

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

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

修改

实际上,一切都不是来自单元测试,而是来自真正的mvc应用程序。在那里,我使用UrlHelper.Action Method (String, Object)来生成传出网址。由于此方法用于布局视图(大多数视图的父视图),我已将其纳入我的扩展帮助方法(从视图中排除额外的逻辑),此扩展方法从中提取操作名称请求上下文,作为参数传递给它。我知道我可以通过请求上下文提取所有当前路由值并替换那些(或者我可以创建一个匿名路由值集合,包含来自的所有值上下文),但我认为这是多余的,因为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 }));
}

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


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

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


修改

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

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

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

  1. 我在上下文中有string个,但是通过匿名对象提供了int,它以某种方式混淆了mvc并且它无法使用route1
  2. 我将匿名对象中的int更改为string s,并且上下文中monthyear的网址与匿名对象中的网址相同,开始正确生成;而其他所有人都没有。
  3. 因此,我看到一条规则:匿名对象的属性应该是string类型,以匹配请求上下文中路由值的类型。

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


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

2 个答案:

答案 0 :(得分: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"]));
}

我只是删除month中的yearcontext.RouteData.Values条目。我只需替换monthyear条目在请求上下文。如果从上下文中删除它们(正如我之前所做的那样),它们将不适用于在此之后调用的助手方法。

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


<强> FINALY

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

第2部分ASP.NET MVC详细

第11章网址,路由和区域

生成外发网址

部分了解段变量重用

  

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

month-year之后的controller段而言,我确实为monthyear指定了值,所有尾随段(actionuser)不会被重复使用。就我而言,也没有在我的匿名对象中指定它们,它们似乎无法用于路线。所以,route1无法匹配。

在书中甚至还有一个警告:

  

处理此行为的最佳方法是阻止它   发生。我们强烈建议您不要依赖此   行为,并为所有段变量提供值   在URL模式中。依靠这种行为不仅会让你的   代码难以阅读,但您最终会对订单做出假设   您的用户在其中发出请求,这是可以的   当您的应用程序进入维护状态时,最终会咬你。

嗯,它咬了我)))

值得记住丢失100个代表(我将在此再次重复)规则:路由系统将仅重用URL模式中较早出现的段变量的值,而不是任何提供的参数。< /强>

答案 1 :(得分:-1)

一条具有默认用户的路由是否足够?那是

routes.MapRoute(
    "route1",
    "{controller}/{month}-{year}/{action}/{user}",
    new { user = "" }
);

否则,您必须允许更具体的用户路线,以便在创建URL时不会首先匹配route2。

更新:我建议你离开你的助手并直接使用Url.Action,这里有两个针对你的场景的测试:

 [TestFixture]
    public class RouteRegistrarBespokeTests
    {
        private UrlHelper _urlHelper;

        [SetUp]
        public void SetUp()
        {
            var routes = new RouteCollection();
            routes.Clear();
            var routeData = new RouteData();
            RegisterRoutesTo(routes);
            var requestContext = new RequestContext(HttpMocks.HttpContext(), 
                                           routeData);
            _urlHelper = new UrlHelper(requestContext, routes);
        }

        [Test]
        public void Should_be_able_to_map_sample_without_user()
        {
            var now = DateTime.Now;
            var result = _urlHelper.Action("Index", "Sample", 
                               new {  year = now.Year, month = now.Month });
            Assert.AreEqual(string.Format("/Sample/{0}-{1}/Index", 
                                      now.Month, now.Year ), result);
        }

        [Test]
        public void Should_be_able_to_map_sample_with_user()
        {
            var now = DateTime.Now;
            var result = _urlHelper.Action("Index", "Sample", 
                          new { user = "user1", year = now.Year, 
                                month = now.Month });
            Assert.AreEqual(string.Format("/Sample/{0}-{1}/Index/{2}", 
                                     now.Month, now.Year, "user1"), result);
        }



private static void RegisterRoutesTo(RouteCollection routes)
{
    routes.MapRoute("route1", "{controller}/{month}-{year}/{action}/{user}");
    routes.MapRoute("route2", "{controller}/{month}-{year}/{action}");
}

}