Можно ли использовать ASP.NET Routing для создания «чистых» URL-адресов для обработчиков .ashx (IHttpHander)? - PullRequest
26 голосов
/ 29 июля 2010

У меня есть некоторые REST-сервисы, использующие старые IHttpHandler s.Я хотел бы создать более чистые URL, чтобы у меня не было .ashx в пути.Есть ли способ использовать маршрутизацию ASP.NET для создания маршрутов, которые сопоставляются с обработчиками Ashx?Ранее я видел эти типы маршрутов:

// Route to an aspx page
RouteTable.Routes.MapPageRoute("route-name",
    "some/path/{arg}",
    "~/Pages/SomePage.aspx");

// Route for a WCF service
RouteTable.Routes.Add(new ServiceRoute("Services/SomeService",
    new WebServiceHostFactory(),
    typeof(SomeService)));

При попытке использовать RouteTable.Routes.MapPageRoute() выдается ошибка (что обработчик не выводится из Page).System.Web.Routing.RouteBase имеет только 2 производных класса: ServiceRoute для сервисов и DynamicDataRoute для MVC.Я не уверен, что делает MapPageRoute() (Reflector не показывает тело метода, он просто показывает «Производительность, критичная для встраивания этого типа метода через границы изображения NGen»).

Я вижу, что RouteBase не запечатан и имеет относительно простой интерфейс:

public abstract RouteData GetRouteData(HttpContextBase httpContext);

public abstract VirtualPathData GetVirtualPath(RequestContext requestContext,
    RouteValueDictionary values);

Так что, возможно, я смогу сделать свой собственный HttpHandlerRoute.Я попробую, но если кто-нибудь знает о существующем или встроенном способе сопоставления маршрутов с IHttpHandlers, это было бы здорово.

Ответы [ 5 ]

26 голосов
/ 01 августа 2010

Хорошо, я понял это с тех пор, как первоначально задал вопрос, и у меня наконец есть решение, которое делает именно то, что я хочу. Немного предварительного объяснения, однако, из-за. IHttpHandler - очень простой интерфейс:

bool IsReusable { get; }
void ProcessRequest(HttpContext context)

Нет встроенного свойства для доступа к данным маршрута, и данные маршрута также не могут быть найдены в контексте или запросе. Объект System.Web.UI.Page имеет свойство RouteData, ServiceRoute s выполняет всю работу по интерпретации ваших UriTemplates и передаче значений в правильный метод внутри, а ASP.NET MVC предоставляет свой собственный способ доступа к данным маршрута. Даже если у вас было RouteBase, которое (а) определило, соответствует ли входящий URL-адрес вашему маршруту, и (б) проанализировало URL-адрес, чтобы извлечь все отдельные значения, которые будут использоваться из вашего IHttpHandler, нет простого способа передать эти данные маршрута вашему IHttpHandler. Если вы хотите, чтобы ваш IHttpHandler был «чистым», так сказать, он берет на себя ответственность за работу с URL и за то, как извлечь из него любые значения. Реализация RouteBase в этом случае используется только для определения того, должен ли вообще использоваться ваш IHttpHandler.

Однако остается одна проблема. Как только RouteBase определяет, что входящий URL-адрес соответствует вашему маршруту, он передается IRouteHandler, который создает экземпляры IHttpHandler, которые вы хотите обработать по вашему запросу. Но, как только вы в вашем IHttpHandler, значение context.Request.CurrentExecutionFilePath вводит в заблуждение. Это URL, полученный от клиента, за вычетом строки запроса. Так что это не путь к вашему файлу .ashx. И любые части вашего маршрута, которые являются постоянными (например, имя метода), будут частью значения пути этого исполняемого файла. Это может быть проблемой, если вы используете UriTemplates в своем IHttpHandler, чтобы определить, какой конкретный метод в вашем IHttpHandler должен обрабатывать запрос.

Пример: если у вас был обработчик .ashx по адресу /myApp/services/myHelloWorldHandler.ashx И у вас был этот маршрут, который сопоставлен с обработчиком: "services / hello / {name}" И вы перешли на этот URL, пытаясь вызвать метод SayHello(string name) вашего обработчика: http://localhost/myApp/services/hello/SayHello/Sam

Тогда ваш CurrentExecutionFilePath будет: / myApp / services / hello / Sam. Он включает в себя части URL маршрута, что является проблемой. Вы хотите, чтобы путь к исполняемому файлу соответствовал вашему URL-адресу маршрута. Приведенные ниже реализации RouteBase и IRouteHandler решают эту проблему.

Перед тем, как вставить 2 класса, вот очень простой пример использования. Обратите внимание, что эти реализации RouteBase и IRouteHandler фактически будут работать для IHttpHandlers, у которых даже нет файла .ashx, что довольно удобно.

// A "headless" IHttpHandler route (no .ashx file required)
RouteTable.Routes.Add(new GenericHandlerRoute<HeadlessService>("services/headless"));

Это приведет к тому, что все входящие URL, которые соответствуют маршруту "services / headless", будут переданы новому экземпляру HeadlessService IHttpHandler (HeadlessService - просто пример в этом случае. Это будет любая реализация IHttpHandler, которую вы хотели выдать).

Хорошо, вот реализации класса маршрутизации, комментарии и все:

/// <summary>
/// For info on subclassing RouteBase, check Pro Asp.NET MVC Framework, page 252.
/// Google books link: http://books.google.com/books?id=tD3FfFcnJxYC&pg=PA251&lpg=PA251&dq=.net+RouteBase&source=bl&ots=IQhFwmGOVw&sig=0TgcFFgWyFRVpXgfGY1dIUc0VX4&hl=en&ei=z61UTMKwF4aWsgPHs7XbAg&sa=X&oi=book_result&ct=result&resnum=6&ved=0CC4Q6AEwBQ#v=onepage&q=.net%20RouteBase&f=false
/// 
/// It explains how the asp.net runtime will call GetRouteData() for every route in the route table.
/// GetRouteData() is used for inbound url matching, and should return null for a negative match (the current requests url doesn't match the route).
/// If it does match, it returns a RouteData object describing the handler that should be used for that request, along with any data values (stored in RouteData.Values) that
/// that handler might be interested in.
/// 
/// The book also explains that GetVirtualPath() (used for outbound url generation) is called for each route in the route table, but that is not my experience,
/// as mine used to simply throw a NotImplementedException, and that never caused a problem for me.  In my case, I don't need to do outbound url generation,
/// so I don't have to worry about it in any case.
/// </summary>
/// <typeparam name="T"></typeparam>
public class GenericHandlerRoute<T> : RouteBase where T : IHttpHandler, new()
{
    public string RouteUrl { get; set; }


    public GenericHandlerRoute(string routeUrl)
    {
        RouteUrl = routeUrl;
    }


    public override RouteData GetRouteData(HttpContextBase httpContext)
    {
        // See if the current request matches this route's url
        string baseUrl = httpContext.Request.CurrentExecutionFilePath;
        int ix = baseUrl.IndexOf(RouteUrl);
        if (ix == -1)
            // Doesn't match this route.  Returning null indicates to the asp.net runtime that this route doesn't apply for the current request.
            return null;

        baseUrl = baseUrl.Substring(0, ix + RouteUrl.Length);

        // This is kind of a hack.  There's no way to access the route data (or even the route url) from an IHttpHandler (which has a very basic interface).
        // We need to store the "base" url somewhere, including parts of the route url that are constant, like maybe the name of a method, etc.
        // For instance, if the route url "myService/myMethod/{myArg}", and the request url were "http://localhost/myApp/myService/myMethod/argValue",
        // the "current execution path" would include the "myServer/myMethod" as part of the url, which is incorrect (and it will prevent your UriTemplates from matching).
        // Since at this point in the exectuion, we know the route url, we can calculate the true base url (excluding all parts of the route url).
        // This means that any IHttpHandlers that use this routing mechanism will have to look for the "__baseUrl" item in the HttpContext.Current.Items bag.
        // TODO: Another way to solve this would be to create a subclass of IHttpHandler that has a BaseUrl property that can be set, and only let this route handler
        // work with instances of the subclass.  Perhaps I can just have RestHttpHandler have that property.  My reticence is that it would be nice to have a generic
        // route handler that works for any "plain ol" IHttpHandler (even though in this case, you have to use the "global" base url that's stored in HttpContext.Current.Items...)
        // Oh well.  At least this works for now.
        httpContext.Items["__baseUrl"] = baseUrl;

        GenericHandlerRouteHandler<T> routeHandler = new GenericHandlerRouteHandler<T>();
        RouteData rdata = new RouteData(this, routeHandler);

        return rdata;
    }


    public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
    {
        // This route entry doesn't generate outbound Urls.
        return null;
    }
}



public class GenericHandlerRouteHandler<T> : IRouteHandler where T : IHttpHandler, new()
{
    public IHttpHandler GetHttpHandler(RequestContext requestContext)
    {
        return new T();
    }
}

Я знаю, что этот ответ был довольно длинным, но решить его было непросто. Основная логика была достаточно простой, хитрость заключалась в том, чтобы каким-то образом заставить ваш IHttpHandler знать о «базовом URL», чтобы он мог правильно определить, какие части URL-адреса принадлежат маршруту, а какие являются фактическими аргументами для вызова службы.

Эти классы будут использоваться в моей следующей библиотеке CEST REST, RestCake . Я надеюсь, что мой путь по кроличьей норе поможет другим, кто решит использовать RouteBase, и будет делать классные вещи с IHttpHandlers.

13 голосов
/ 11 августа 2010

Мне действительно нравится решение Джоэла, так как оно не требует, чтобы вы знали тип обработчика, пока вы пытаетесь настроить маршруты.Я бы проголосовал за это, но, увы, у меня нет требуемой репутации.

Я действительно нашел решение, которое, как мне кажется, лучше, чем оба упомянутых.Исходный исходный код, из которого я получил мой пример, можно найти по ссылке здесь http://weblogs.asp.net/leftslipper/archive/2009/10/07/introducing-smartyroute-a-smarty-ier-way-to-do-routing-in-asp-net-applications.aspx.

Это меньше кода, независимый от типа и быстрый.

public class HttpHandlerRoute : IRouteHandler {

  private String _VirtualPath = null;

  public HttpHandlerRoute(String virtualPath) {
    _VirtualPath = virtualPath;
  }

  public IHttpHandler GetHttpHandler(RequestContext requestContext) {
    IHttpHandler httpHandler = (IHttpHandler)BuildManager.CreateInstanceFromVirtualPath(_VirtualPath, typeof(IHttpHandler));
    return httpHandler;
  }
}

И примерный пример использования

String handlerPath = "~/UploadHandler.ashx";
RouteTable.Routes.Add(new Route("files/upload", new HttpHandlerRoute(handlerPath)));
9 голосов
/ 21 октября 2011

EDIT: я только что отредактировал этот код, потому что у меня были некоторые проблемы со старым.Если вы используете старую версию, пожалуйста, обновите.

Этот поток немного староват, но я просто переписал часть кода здесь, чтобы сделать то же самое, но более элегантно, используя метод расширения.

Я использую это на веб-формах ASP.net, и мне нравится размещать файлы Ashx в папке и иметь возможность вызывать их, используя маршрутизацию или обычный запрос.

ИтакЯ в значительной степени схватил код shellscape и сделал метод расширения, который добился цели.В конце я почувствовал, что должен также поддерживать передачу объекта IHttpHandler вместо его URL-адреса, поэтому для этого я написал и перегрузил метод MapHttpHandlerRoute.

namespace System.Web.Routing
{
 public class HttpHandlerRoute<T> : IRouteHandler where T: IHttpHandler
 {
  private String _virtualPath = null;

  public HttpHandlerRoute(String virtualPath)
  {
   _virtualPath = virtualPath;
  }

  public HttpHandlerRoute() { }

  public IHttpHandler GetHttpHandler(RequestContext requestContext)
  {
   return Activator.CreateInstance<T>();
  }
 }

 public class HttpHandlerRoute : IRouteHandler
 {
  private String _virtualPath = null;

  public HttpHandlerRoute(String virtualPath)
  {
   _virtualPath = virtualPath;
  }

  public IHttpHandler GetHttpHandler(RequestContext requestContext)
  {
   if (!string.IsNullOrEmpty(_virtualPath))
   {
    return (IHttpHandler)System.Web.Compilation.BuildManager.CreateInstanceFromVirtualPath(_virtualPath, typeof(IHttpHandler));
   }
   else
   {
    throw new InvalidOperationException("HttpHandlerRoute threw an error because the virtual path to the HttpHandler is null or empty.");
   }
  }
 }

 public static class RoutingExtension
 {
  public static void MapHttpHandlerRoute(this RouteCollection routes, string routeName, string routeUrl, string physicalFile, RouteValueDictionary defaults = null, RouteValueDictionary constraints = null)
  {
   var route = new Route(routeUrl, defaults, constraints, new HttpHandlerRoute(physicalFile));
   routes.Add(routeName, route);
  }

  public static void MapHttpHandlerRoute<T>(this RouteCollection routes, string routeName, string routeUrl, RouteValueDictionary defaults = null, RouteValueDictionary constraints = null) where T : IHttpHandler
  {
   var route = new Route(routeUrl, defaults, constraints, new HttpHandlerRoute<T>());
   routes.Add(routeName, route);
  }
 }
}

Я помещаю его в то же пространство именвсе собственные объекты маршрутизации, так что они будут автоматически доступны.

Так что, чтобы использовать это, вам просто нужно позвонить:

// using the handler url
routes.MapHttpHandlerRoute("DoSomething", "Handlers/DoSomething", "~/DoSomething.ashx");

Или

// using the type of the handler
routes.MapHttpHandlerRoute<MyHttpHanler>("DoSomething", "Handlers/DoSomething");

Наслаждайтесь, Алекс

5 голосов
/ 04 декабря 2010

Все эти ответы очень хороши. Я люблю простоту класса мистера Мичама GenericHandlerRouteHandler<T>. Это хорошая идея, чтобы исключить ненужную ссылку на виртуальный путь, если вы знаете конкретный класс HttpHandler. Однако класс GenericHandlerRoute<T> не нужен. Существующий класс Route, производный от RouteBase, уже обрабатывает всю сложность сопоставления маршрутов, параметров и т. Д., Поэтому мы можем просто использовать его вместе с GenericHandlerRouteHandler<T>.

Ниже приведена комбинированная версия с реальным примером использования, который включает параметры маршрута.

Сначала идут обработчики маршрута. Здесь есть два включенных - оба с одним и тем же именем класса, но одно общее и использующее информацию о типе для создания экземпляра конкретного HttpHandler, как при использовании г-на Мичама, и другое, которое использует виртуальный путь и BuildManager для создания экземпляра соответствующего HttpHandler, как при использовании shellscape. Хорошая новость заключается в том, что .NET позволяет обеим сторонам просто прекрасно жить, поэтому мы можем просто использовать все, что захотим, и переключаться между ними по своему желанию.

using System.Web;
using System.Web.Compilation;
using System.Web.Routing;

public class HttpHandlerRouteHandler<T> : IRouteHandler where T : IHttpHandler, new() {

  public HttpHandlerRouteHandler() { }

  public IHttpHandler GetHttpHandler(RequestContext requestContext) {
    return new T();
  }
}

public class HttpHandlerRouteHandler : IRouteHandler {

  private string _VirtualPath;

  public HttpHandlerRouteHandler(string virtualPath) {
    this._VirtualPath = virtualPath;
  }

  public IHttpHandler GetHttpHandler(RequestContext requestContext) {
    return (IHttpHandler) BuildManager.CreateInstanceFromVirtualPath(this._VirtualPath, typeof(IHttpHandler));
  }

}

Давайте предположим, что мы создали HttpHandler, который передает документы пользователям из ресурса, находящегося за пределами нашей виртуальной папки, возможно, даже из базы данных, и что мы хотим обмануть браузер пользователя, заставив его поверить, что мы непосредственно обслуживаем определенный файл вместо того, чтобы просто обеспечить загрузку (т. е. позволить подключаемым модулям браузера обрабатывать файл, а не заставлять пользователя сохранять файл). HttpHandler может ожидать, что идентификатор документа будет использоваться для поиска документа, и может ожидать, что имя файла будет предоставлено браузеру - имя, которое может отличаться от имени файла, используемого на сервере.

Ниже показана регистрация маршрута, используемого для достижения этого с DocumentHandler HttpHandler:

routes.Add("Document", new Route("document/{documentId}/{*fileName}", new HttpHandlerRouteHandler<DocumentHandler>()));

Я использовал {*fileName}, а не просто {fileName}, чтобы параметр fileName действовал как необязательный параметр catch-all.

Чтобы создать URL для файла, обслуживаемого этим HttpHandler, мы можем добавить следующий статический метод в класс, где такой метод будет уместен, например, в самом классе HttpHandler:

public static string GetFileUrl(int documentId, string fileName) {
  string mimeType = null;
  try { mimeType = MimeMap.GetMimeType(Path.GetExtension(fileName)); }
  catch { }
  RouteValueDictionary documentRouteParameters = new RouteValueDictionary {   { "documentId", documentId.ToString(CultureInfo.InvariantCulture) }
                                                                            , { "fileName",   DocumentHandler.IsPassThruMimeType(mimeType) ? fileName : string.Empty } };
  return RouteTable.Routes.GetVirtualPath(null, "Document", documentRouteParameters).VirtualPath;
}

Я опустил определения MimeMap и и IsPassThruMimeType, чтобы упростить этот пример. Но они предназначены для определения того, должны ли определенные типы файлов предоставлять свои имена файлов непосредственно в URL, или, скорее, в заголовке HTTP Content-Disposition. Некоторые расширения файлов могут быть заблокированы IIS или сканированием URL-адресов или могут привести к выполнению кода, который может вызвать проблемы у пользователей, особенно если источником файла является другой пользователь, который является вредоносным. Вы можете заменить эту логику другой фильтрующей логикой или полностью ее исключить, если вы не подвержены этому типу риска.

Поскольку в этом конкретном примере имя файла может быть опущено в URL, то, очевидно, мы должны извлечь имя файла откуда-то. В этом конкретном примере имя файла можно получить, выполнив поиск с использованием идентификатора документа, и включение имени файла в URL предназначено исключительно для улучшения взаимодействия с пользователем. Таким образом, DocumentHandler HttpHandler может определить, было ли указано имя файла в URL, а если нет, то он может просто добавить HTTP-заголовок Content-Disposition к ответу.

Оставаясь в теме , важной частью приведенного выше блока кода является использование RouteTable.Routes.GetVirtualPath() и параметров маршрутизации для генерации URL-адреса из объекта Route, который мы создали в процессе регистрации маршрута. .

Вот расширенная версия класса DocumentHandler HttpHandler (для ясности многое опущено). Вы можете видеть, что этот класс использует параметры маршрута для получения идентификатора документа и имени файла, когда это возможно; в противном случае он будет пытаться получить идентификатор документа из параметра строки запроса (т.е. при условии, что маршрутизация не использовалась).

public void ProcessRequest(HttpContext context) {

  try {

    context.Response.Clear();

    // Get the requested document ID from routing data, if routed.  Otherwise, use the query string.
    bool    isRouted    = false;
    int?    documentId  = null;
    string  fileName    = null;
    RequestContext requestContext = context.Request.RequestContext;
    if (requestContext != null && requestContext.RouteData != null) {
      documentId  = Utility.ParseInt32(requestContext.RouteData.Values["documentId"] as string);
      fileName    = Utility.Trim(requestContext.RouteData.Values["fileName"] as string);
      isRouted    = documentId.HasValue;
    }

    // Try the query string if no documentId obtained from route parameters.
    if (!isRouted) {
      documentId  = Utility.ParseInt32(context.Request.QueryString["id"]);
      fileName    = null;
    }
    if (!documentId.HasValue) { // Bad request
      // Response logic for bad request omitted for sake of simplicity
      return;
    }

    DocumentDetails documentInfo = ... // Details of loading this information omitted

    if (context.Response.IsClientConnected) {

      string fileExtension = string.Empty;
      try { fileExtension = Path.GetExtension(fileName ?? documentInfo.FileName); } // Use file name provided in URL, if provided, to get the extension.
      catch { }

      // Transmit the file to the client.
      FileInfo file = new FileInfo(documentInfo.StoragePath);
      using (FileStream fileStream = file.OpenRead()) {

        // If the file size exceeds the threshold specified in the system settings, then we will send the file to the client in chunks.
        bool mustChunk = fileStream.Length > Math.Max(SystemSettings.Default.MaxBufferedDownloadSize * 1024, DocumentHandler.SecondaryBufferSize);

        // WARNING! Do not ever set the following property to false!
        //          Doing so causes each chunk sent by IIS to be of the same size,
        //          even if a chunk you are writing, such as the final chunk, may
        //          be shorter than the rest, causing extra bytes to be written to
        //          the stream.
        context.Response.BufferOutput   = true;

        context.Response.ContentType = MimeMap.GetMimeType(fileExtension);
        context.Response.AddHeader("Content-Length", fileStream.Length.ToString(CultureInfo.InvariantCulture));
        if (   !isRouted
            || string.IsNullOrWhiteSpace(fileName)
            || string.IsNullOrWhiteSpace(fileExtension)) {  // If routed and a file name was provided in the route, then the URL will appear to point directly to a file, and no file name header is needed; otherwise, add the header.
          context.Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", HttpUtility.UrlEncode(documentInfo.FileName)));
        }

        int     bufferSize      = DocumentHandler.SecondaryBufferSize;
        byte[]  buffer          = new byte[bufferSize];
        int     bytesRead       = 0;

        while ((bytesRead = fileStream.Read(buffer, 0, bufferSize)) > 0 && context.Response.IsClientConnected) {
          context.Response.OutputStream.Write(buffer, 0, bytesRead);
          if (mustChunk) {
            context.Response.Flush();
          }
        }
      }

    }

  }
  catch (Exception e) {
    // Error handling omitted from this example.
  }
}

В этом примере используются некоторые дополнительные пользовательские классы, такие как класс Utility, для упрощения некоторых тривиальных задач.Но, надеюсь, вы сможете пройти через это.Конечно, единственная действительно важная часть этого класса в отношении текущей темы - это получение параметров маршрута из context.Request.RequestContext.RouteData.Но я видел несколько постов в других местах, в которых спрашивалось, как транслировать большие файлы, используя HttpHandler, не перегружая память сервера, поэтому было неплохо объединить примеры.

4 голосов
/ 30 июля 2010

Да, я тоже это заметил.Возможно, есть встроенный способ ASP.NET, чтобы сделать это, но уловка для меня заключалась в создании нового класса, производного от IRouteHandler:

using System;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Routing;

namespace MyNamespace
{
    class GenericHandlerRouteHandler : IRouteHandler
    {
        private string _virtualPath;
        private Type _handlerType;
        private static object s_lock = new object();

        public GenericHandlerRouteHandler(string virtualPath)
        {
            _virtualPath = virtualPath;
        }

        #region IRouteHandler Members

        public System.Web.IHttpHandler GetHttpHandler(RequestContext requestContext)
        {
            ResolveHandler();

            IHttpHandler handler = (IHttpHandler)Activator.CreateInstance(_handlerType);
            return handler;
        }

        #endregion

        private void ResolveHandler()
        {
            if (_handlerType != null)
                return;

            lock (s_lock)
            {
                // determine physical path of ashx
                string path = _virtualPath.Replace("~/", HttpRuntime.AppDomainAppPath);

                if (!File.Exists(path))
                    throw new FileNotFoundException("Generic handler " + _virtualPath + " could not be found.");

                // parse the class name out of the .ashx file
                // unescaped reg-ex: (?<=Class=")[a-zA-Z\.]*
                string className;
                Regex regex = new Regex("(?<=Class=\")[a-zA-Z\\.]*");
                using (var sr = new StreamReader(path))
                {
                    string str = sr.ReadToEnd();

                    Match match = regex.Match(str);
                    if (match == null)
                        throw new InvalidDataException("Could not determine class name for generic handler " + _virtualPath);

                    className = match.Value;
                }

                // get the class type from the name
                Assembly[] asms = AppDomain.CurrentDomain.GetAssemblies();
                foreach (Assembly asm in asms)
                {
                    _handlerType = asm.GetType(className);
                    if (_handlerType != null)
                        break;
                }

                if (_handlerType == null)
                    throw new InvalidDataException("Could not find type " + className + " in any loaded assemblies.");
            }
        }
    }
}

Чтобы создать маршрут для .ashx:

IRouteHandler routeHandler = new GenericHandlerRouteHandler("~/somehandler.ashx");
Route route = new Route("myroute", null, null, null, routeHandler);
RouteTable.Routes.Add(route);

Приведенный выше код может нуждаться в улучшении для работы с аргументами маршрута, но это отправная точка.Комментарии приветствуются.

...