Решение проблемы с двумя параметрами в маршруте

Жалко признавать, но ASP.NET MVC 3 сделал шаг назад в плане маршрутизации, по сравнению с  ASP.NET MVC 2. Однако не все так печально и есть простой способ решить появившуюся проблему.

Проблема

Рассматриваемая далее ошибка проявляется себя в том случае, если указаны два последовательных необязательных параметра. Необходимо отметить, что на входящие запросы это никак не влияет и они продолжают работать без проблем. Но рассмотрим обратную ситуацию, когда с помощью данного маршрута необходимо создать URL.

Например, в веб-приложении задан следующий маршрут:

routes.MapRoute("by-day",
    "archive/{month}/{day}",
    new { 
        controller = "Home",
        action = "Index", 
        month = UrlParameter.Optional,
        day = UrlParameter.Optional 
    }
);

Обратите внимание, что оба параметра month и day объявлены как опциональные.

Теперь предположим что в веб-приложении встречаются следующие выражения для создания URL с использованием указанного маршрута:

@Url.RouteUrl("by-day", new { month = 1, day = 23 })
@Url.RouteUrl("by-day", new { month = 1 })
@Url.RouteUrl("by-day", null)

Что выдает в этом случае эквивалент данного кода в ASP.NET 2? (Речь именно о эквиваленте, т.к. движок представлений Razor не существует в ASP.NET MVC 2, но данный пример легко переписать на WebFоrms). Ответ будет вполне ожидаемым:

/archive/1/23 
/archive/1 
/archive 

Теперь посмотрим на результаты ASP.NET MVC 3:

/archive/1/23 
/archive/1 

Это не опечатка. Третьим результатом будет null. Это как раз и есть следствие рассматриваемой ошибки. Она проявляется, как уже было отмечено, когда в маршруте есть два последовательных опциональных параметра и для них отсутствуют значения при создании URL.

Давайте вначале разберемся как обойти данную ошибку. А затем посмотрим из-за чего она происходит.

Обходной путь

Решение очень простое и заложено в описании самой ошибки. Посмотрим внимательно: "два последовательных необязательных параметра". Таким образом, избавимся от опциональных параметров, убрав значения по умолчанию у month и day. Этот маршрут теперь будет отвечать за первый URL из трех приведенных выше (где эти значения были явно указаны).

Теперь добавим к нему еще один путь для остальных случаев. Он будет содержать только один опциональный параметр – month.

Вот как будут выглядеть эти пути после всех изменений:

routes.MapRoute("by-day",
    "archive/{month}/{day}",
    new { controller = "Home", action = "Index" }
);

outes.MapRoute("by-month",
    "archive/{month}",
    new { controller = "Home", action = "Index", month = UrlParameter.Optional }
);

Теперь изменим сами запросы на создание URL. В двух последних случаях теперь необходимо использовать маршрут под названием "by-month".

@Url.RouteUrl("by-day", new { month = 1, day = 23 })
@Url.RouteUrl("by-month", new { month = 1 })
@Url.RouteUrl("by-month", null)

Для полной ясности ситуации необходимо сделать еще один комментарий: Этой ошибке подвержены все методы создания URL в ASP.NET MVC 3. Поэтому, можно создавать ссылки на Действия например вот так:

@Html.ActionLink("sample", "Index", "Home", new { month = 1, day = 23 }, null)
@Html.ActionLink("sample", "Index", "Home", new { month = 1}, null)
@Html.ActionLink("sample", "Index", "Home")

В этом случае ошибка все равно проявится в третьей строке. И для её устранения необходимо будет использовать приведенный выше обходной манёвр.

Но всё не так плохо как может показаться если изначально следовать практике централизации создания URL. Например, разработчики http://forums.asp.net/ столкнулись с данной проблемой, когда производили обновление до ASP.NET MVC 3. Однако, в их коде вместо вызовов, например, метода ActionLink() во всех Представлениях, использовался вызов специфичных для их задачи методов вроде ForumDetailUrl(). Это позволило обойти ошибку, изменив код только в одном месте.

Разобравшись в чем проблема и как ей обойти, давайте посмотрим почему это происходит в веб-приложениях на базе ASP.NET MVC 3.

Причины

Удовлетворим любопытство и посмотрим в чем причина возникшей ситуации. Вспомним маршрут с опциональными параметрами, который был приведен в самом начале:

routes.MapRoute("by-day",
    "archive/{month}/{day}",
    new { 
        controller = "Home",
        action = "Index", 
        month = UrlParameter.Optional,
        day = UrlParameter.Optional 
    }
);

А теперь обратите внимание, что в течении всей статьи не было попыток создать URL используя только второй из двух необязательных параметров:

@Url.RouteUrl("by-day", new { day = 23 })

Этот вызов обязан закончиться ошибкой, т.к. не было указано значение для month, опционального параметра идущего первым в списке. Если еще кому-то еще не ясно почему это исключительная ситуация, то давайте представим что такой вызов разрешен. Какую ссылку он тогда создаст? Возможно "/archive/23"? Это точно не правильный путь. Ведь его обратное преобразование приведет к тому, что 23 будет трактовано как значение месяца, а не дня.

Если указанный вызов метода RouteUrl() был бы сделан в ASP.NET MVC 2, то он бы дал результат вида "/archive/System.Web.Mvc.UrlParameter/23". Дело в том, что UrlParameter.Optional это класс, реализованный в ASP.NET MVC 2. Он расположен и работает вне ASP.NET Framework. Проще говоря, ядро ASP.NET ничего не знает про этот новый класс, который используется при создании маршрутов в ASP.NET MVC.

Чтобы исправить такое поведение, в ASP.NET MVC 3 переопределили метод ToString() класса UrlParameter.Optional. Теперь он возвращает пустую строку. Это решило ситуацию, описанную в предыдущем абзаце. Однако это выявило другую ошибку, уже в ядре маршрутизации ASP.NET. Она заключалась в том, что маршруты с опциональными параметрами вели себя не корректно, если не получали значения при создании URL. Звучит знакомо?

Теперь, оглядываясь назад можно сказать, что решение исправить первую ошибку было не верным. Это породило проблемы для многих веб-приложений. Некоторые из них даже использовали её в собственных целях. Однако вторая ошибка, связанная с двумя опциональными параметрами, была найдена уже на очень поздней стадии. В этой ситуации выпуск ASP.NET MVC 3 было одним из самых сложных решений, которое принимала команда ASP.NET MVC за все время разработки. Но ведь не всегда программы работают так, как от них ожидают.

Чтобы завершить статью на хорошей ноте необходимо отметить, что теперь ошибка найдена и изучена. И хочется надеяться что она будет исправлена в следующей версии ASP.NET Framework.

Оригинал статьи: Routing Regression With Two Consecutive Optional Url Parameters

Добавить комментарий