На днях представился случай познакомиться с ASP .NET MVC 1.0, о которой уже не раз читал в блогах и здесь, на Хабре. С первого взгляда понравилась простота используемой концепции и логичность связи с архитектурой ASP .NET (привычные aspx, ascx, masterpages, Global.asax — только теперь используемые несколько иначе). Однако, при всех удобствах, способа задания роутинга и передачи параметров в виде {Controller}/key1/value1/key2/value2 я не нашел. Их можно задать сколько угодно, но, к сожалению, при этом они должны стоять строго на указанном месте, а это иногда очень неудобно, особенно при передаче большого количества значений. Ведь какие-то аргументы могут иметь значения по умолчанию, и запихивать их в URL принудительно — не лучшее решение. Конечно, можно было бы воспользоваться стандартным, ?key1=value1&key2=value2 способом, но лично мне почему-то захотелось иметь возможность задавать параметры именно в таком, «MVC-style», если можно так выразиться :)
Оказалось, что вполне, и без особых усилий. Для начала пролистал пару статей по архитектуре ASP .NET MVC, нашел описание основных этапов жизненного цикла запроса: www.asp.net/learn/mvc/tutorial-22-cs.aspx. Судя по описанию, для того, чтобы внедрить свой обработчик роутинга, необходимо заменить либо модифицировать стандартный UrlRoutingModule таким образом, чтобы в случае отсутствия подходящего маршрута производились дополнительные проверки на совпадение с «особыми» роутами, которые будут идентифицировать Action-методы некоторого контроллера с неопределенным числом параметров. Заглянув с помощью Reflector'a в код UrlRoutingModule, можно увидеть, что основная работа, которую выполняет этот класс — обработка событий Application.PostMapRequestHandler и Application.PostResolveRequestCache. Код метода, которому делегируется выполнение обработки события, наводит на мысль относительно безопасной для работы существующего кода модификации:
Далее, если мы будем добавлять особую логику в этот метод, то нам необходимо отличать наши, «особые» роуты в таблице RouteTable от обычных. Для этого я использовал сигнатуру {...}, которая обозначает, что в этом месте может быть любое число параметров, которые будут организованы парами /key/value, то есть для шаблона MyExtendedController/{...} будут справедливы URL, например, такого плана: /MyExtendedController/searchBy/name/page/3/pageSize/15
Итак, задача — добавить код, который возьмет Request из контекста context, проверит его на соответствие одному из «особых» роутов, и подсунет фейковый routeData вместо легального null, что приведет к передаче управления нужному контроллеру и его Action-методу. Затем нужно прикрепить модифицированный код к приложению. Здесь можно было пойти несколькими путями, первый — модификация сборки System.Web.Routing — отметаем сразу за ненужными сложностями и неудобством, второй — наследование от UrlRoutingModule и переопределение соответствующего виртуального метода — годится, но я выбрал третий путь, а именно, выдирание кода UrlRoutingModule из сборки System.Web.Routing рефлектором (поскольку посторонних зависимостей это не несет) и простая модификация нужных методов. Все прошло успешно, добавленный код выглядит следующим образом:
здесь RouteGhost — подставной класс, наследуемый от Route, куда мы можем скопировать все данные из проверяемого Route и проверить, не подойдет ли он нам. В RouteGhost переопределен метод
который как раз и проверяет соответствие запроса из context.Request паттерну Route.Url с учетом {...}.
Итак, наш модуль-аналог UrlRoutingModule создан, и теперь для того, чтобы зарегистрировать его в приложении, в конфиге web.config достаточно поправить одну строчку:
и аналогичную строку в секции в конфигурации <system.webServer>, заменив тип на свой и указав на свою сборку. После этого все должно работать в том же режиме, что и раньше, за исключением того, что теперь помимо стандартного способа разрешения маршрутов у нас есть свой расширенный механизм, который мы можем кастомизовать, как мы хотим.
Для демонстрации я набросал микропроект на базе стандартного хеловорлда-шаблона из поставки ASP .NET MVC, создал тестовый контроллер TestController:
и View
В силу того, что наш расширенный роутинг работает только в том случае, если не было найдено совпадения с обычными, «нормальными» маршрутами, пришлось изменить стандартный
на последовательность
иначе при запросе /Test/ или /Test/param1/value1/ срабатывал бы стандартный {controller}/{action}/{method}.
Ну и в конце добавляем
Запускаем, все работает!
Сам тестовый проект можно скачать здесь
Оказалось, что вполне, и без особых усилий. Для начала пролистал пару статей по архитектуре ASP .NET MVC, нашел описание основных этапов жизненного цикла запроса: www.asp.net/learn/mvc/tutorial-22-cs.aspx. Судя по описанию, для того, чтобы внедрить свой обработчик роутинга, необходимо заменить либо модифицировать стандартный UrlRoutingModule таким образом, чтобы в случае отсутствия подходящего маршрута производились дополнительные проверки на совпадение с «особыми» роутами, которые будут идентифицировать Action-методы некоторого контроллера с неопределенным числом параметров. Заглянув с помощью Reflector'a в код UrlRoutingModule, можно увидеть, что основная работа, которую выполняет этот класс — обработка событий Application.PostMapRequestHandler и Application.PostResolveRequestCache. Код метода, которому делегируется выполнение обработки события, наводит на мысль относительно безопасной для работы существующего кода модификации:
public virtual void PostResolveRequestCache(HttpContextBase context)
{
RouteData routeData = this.RouteCollection.GetRouteData(context);
// А если routeData == null, то включается наш обработчик
if (routeData != null)
{
IRouteHandler routeHandler = routeData.RouteHandler;
if (routeHandler == null)
{
throw new InvalidOperationException(string.Format(...));
}
if (!(routeHandler is StopRoutingHandler))
{
RequestContext requestContext = new RequestContext(context, routeData);
IHttpHandler httpHandler = routeHandler.GetHttpHandler(requestContext);
...
}
}
}
* This source code was highlighted with Source Code Highlighter.
Далее, если мы будем добавлять особую логику в этот метод, то нам необходимо отличать наши, «особые» роуты в таблице RouteTable от обычных. Для этого я использовал сигнатуру {...}, которая обозначает, что в этом месте может быть любое число параметров, которые будут организованы парами /key/value, то есть для шаблона MyExtendedController/{...} будут справедливы URL, например, такого плана: /MyExtendedController/searchBy/name/page/3/pageSize/15
Итак, задача — добавить код, который возьмет Request из контекста context, проверит его на соответствие одному из «особых» роутов, и подсунет фейковый routeData вместо легального null, что приведет к передаче управления нужному контроллеру и его Action-методу. Затем нужно прикрепить модифицированный код к приложению. Здесь можно было пойти несколькими путями, первый — модификация сборки System.Web.Routing — отметаем сразу за ненужными сложностями и неудобством, второй — наследование от UrlRoutingModule и переопределение соответствующего виртуального метода — годится, но я выбрал третий путь, а именно, выдирание кода UrlRoutingModule из сборки System.Web.Routing рефлектором (поскольку посторонних зависимостей это не несет) и простая модификация нужных методов. Все прошло успешно, добавленный код выглядит следующим образом:
// Try to find an extended routing from routes table
if (routeData == null) {
foreach (RouteBase routeBase in this.RouteCollection) {
if (routeBase is Route) {
Route route = routeBase as Route;
RouteGhost routeGhost = new RouteGhost(route.Url, route.Defaults, route.Constraints, route.DataTokens, route.RouteHandler);
if ((routeData = routeGhost.GetRouteData(context)) != null) {
break;
}
}
}
}
* This source code was highlighted with Source Code Highlighter.
здесь RouteGhost — подставной класс, наследуемый от Route, куда мы можем скопировать все данные из проверяемого Route и проверить, не подойдет ли он нам. В RouteGhost переопределен метод
RouteData GetRouteData(HttpContextBase httpContext)
* This source code was highlighted with Source Code Highlighter.
который как раз и проверяет соответствие запроса из context.Request паттерну Route.Url с учетом {...}.
Итак, наш модуль-аналог UrlRoutingModule создан, и теперь для того, чтобы зарегистрировать его в приложении, в конфиге web.config достаточно поправить одну строчку:
<httpModules>
<add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions,
Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing,
Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
<httpModules>
* This source code was highlighted with Source Code Highlighter.
и аналогичную строку в секции в конфигурации <system.webServer>, заменив тип на свой и указав на свою сборку. После этого все должно работать в том же режиме, что и раньше, за исключением того, что теперь помимо стандартного способа разрешения маршрутов у нас есть свой расширенный механизм, который мы можем кастомизовать, как мы хотим.
Для демонстрации я набросал микропроект на базе стандартного хеловорлда-шаблона из поставки ASP .NET MVC, создал тестовый контроллер TestController:
public class TestController : Controller
{
public ActionResult Index(int? param1, string param2, string param3)
{
ViewData["param1"] = (param1 == null) ? "null" : param1.ToString();
ViewData["param2"] = param2 ?? "null";
ViewData["param3"] = param3 ?? "null";
//
return View("Test");
}
}
* This source code was highlighted with Source Code Highlighter.
и View
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h2>Test</h2>
<b>param1 = <%= ViewData["param1"] %></b>
<br />
<b>param2 = <%= ViewData["param2"] %></b>
<br />
<b>param3 = <%= ViewData["param3"] %></b>
</asp:Content>
* This source code was highlighted with Source Code Highlighter.
В силу того, что наш расширенный роутинг работает только в том случае, если не было найдено совпадения с обычными, «нормальными» маршрутами, пришлось изменить стандартный
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
* This source code was highlighted with Source Code Highlighter.
на последовательность
routes.MapRoute(
Account_Default",
Account/{action}/{id}",
new { controller = "Account", action = "Index", id = "" }
);
routes.MapRoute(
"Home_Default",
"Home/{action}/{id}",
new { controller = "Home", action = "Index", id = "" }
);
routes.MapRoute(
"Default",
"{controller}",
new { controller = "Home", action = "Index", id = "" }
);
* This source code was highlighted with Source Code Highlighter.
иначе при запросе /Test/ или /Test/param1/value1/ срабатывал бы стандартный {controller}/{action}/{method}.
Ну и в конце добавляем
routes.MapRoute(
"Test_Extended",
"Test/{...}",
new { controller = "Test", action = "Index", id = "" }
);
* This source code was highlighted with Source Code Highlighter.
Запускаем, все работает!
Сам тестовый проект можно скачать здесь