Pull to refresh

Простой способ добавить роутинг /key/value в ASP .NET MVC

Reading time6 min
Views2.7K
На днях представился случай познакомиться с 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. Код метода, которому делегируется выполнение обработки события, наводит на мысль относительно безопасной для работы существующего кода модификации:

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.


Запускаем, все работает!

image

Сам тестовый проект можно скачать здесь
Tags:
Hubs:
+5
Comments7

Articles