Маршрутизация в ASP.NET 5

Эта статья была опубликована в корпоративном блоге Microsoft на habrahabr.ru: Ссылка.

Сегодня мы посмотрим на систему маршрутизации в ASP.NET 5.

Как была организована система маршрутизации до ASP.NET 5

Маршрутизация до ASP.NET 5 осуществлялась с помощью ASP.NET модуля UrlRoutingModule. Модуль проходил через коллекцию маршрутов (как правило объектов класса Route) хранящихся в статическом свойстве Routes класса RouteTable, выбирал маршрут, который подходил под текущий запрос и вызывал обработчик маршрута, который хранился в свойстве RouteHandler класса Route - каждый зарегистрированный маршрут мог иметь собственный обработчик. В MVC-приложении этим обработчиком был MvcRouteHandler, который брал на себя дальнейшую работу с запросом.

Маршруты в коллекцию RouteTable.Routes мы добавляли в процессе настройки приложения.

Типичный код настройки системы маршрутизации в MVC приложении:

RouteTable.Routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional });

Где MapRoute - extension-метод, объявленный в пространстве имен System.Web.Mvc, который добавлял в коллекцию маршрутов в свойстве Routes новый маршрут используя MvcRouteHandler в качестве обработчика.

Мы могли бы сделать это и самостоятельно:

RouteTable.Routes.Add(new Route(
    url: "{controller}/{action}/{id}",
    defaults: new RouteValueDictionary(new { controller = "Home", action = "Index", id = UrlParameter.Optional }),
    routeHandler: new MvcRouteHandler()));

Как организована система маршрутизации в ASP.NET 5: Короткий вариант

ASP.NET 5 больше не использует модули, для обработки запросов используются “middleware” введенные в рамках перехода на OWIN - “Open Web Interface” - позволяющей запускать ASP.NET 5 приложения не только на сервере IIS.

Поэтому сейчас маршрутизация осуществляется с помощью RouterMiddleware. Весь проект реализующий маршрутизацию можно загрузить с github. В рамках этой концепции запрос передается от одного middleware к другому, в порядке их регистрации при старте приложения. Когда запрос доходит до RouterMiddleware оно сравнивает подходит ли запрашиваемый Url адрес для какого-нибудь зарегистрированного маршрута, и если подходит, вызывает обработчик этого маршрута.

Как организована система маршрутизации в ASP.NET 5: Длинный вариант

Для того, чтобы разобраться как система маршрутизации работает, давайте подключим ее к пустому проекту ASP.NET 5.

  1. Cоздайте пустой проект ASP.NET 5 (выбрав Empty Template) и назовите его “AspNet5Routing”.

  2. Добавляем в зависимости (“dependencies”) проекта в файле project.json “Microsoft.AspNet.Routing”:

"dependencies": {
    "Microsoft.AspNet.Server.IIS": "1.0.0-beta5",
    "Microsoft.AspNet.Server.WebListener": "1.0.0-beta5",
    "Microsoft.AspNet.Routing": "1.0.0-beta5"
},
  1. В файле Startup.cs добавляем использование пространства имен Microsoft.AspNet.Routing:
using Microsoft.AspNet.Routing;
  1. Добавляем необходимые сервисы (сервисы, которые использует в своей работе система маршрутизации) в методе ConfigureServices() файла Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
    services.AddRouting();
}
  1. И наконец настраиваем систему маршрутизации в методе Configure() файла Startup.cs:
public void Configure(IApplicationBuilder app)
{
    var routeBuilder = new RouteBuilder();
    routeBuilder.DefaultHandler = new ASPNET5RoutingHandler();
    routeBuilder.ServiceProvider = app.ApplicationServices;
    routeBuilder.MapRoute("default", "{controller}/{action}/{id}");
    app.UseRouter(routeBuilder.Build());
}

Взято из примера в проекте маршрутизации.

Разберем последний шаг подробнее:

var routeBuilder = new RouteBuilder();
routeBuilder.DefaultHandler = new ASPNET5RoutingHandler();
routeBuilder.ServiceProvider = app.ApplicationServices;

Создаем экземпляр RouteBuilder и заполняем его свойства. Интерес вызывает свойство DefaultHandler с типом IRouter - судя по названию оно должно содержать обработчик запроса. Я помещаю в него экземпляр ASPNET5RoutingHandler - придуманного мною обработчика запросов, давайте создадим его:

using Microsoft.AspNet.Routing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;

namespace AspNet5Routing
{
    public class ASPNET5RoutingHandler : IRouter
    {
        public VirtualPathData GetVirtualPath(VirtualPathContext context)
        {

        }

        public async Task RouteAsync(RouteContext context)
        {
            await context.HttpContext.Response.WriteAsync("ASPNET5RoutingHandler work");
            context.IsHandled = true;
        }
    }
}

Интерфейс IRouter требует от нас только два метода GetVirtualPath и RouteAsync.

Метод GetVirtualPath - нам знаком из предыдущих версий ASP.NET он был в интерфейсе класса RouteBase от которого наследовался класс Route представляющий собой маршрут. Этот метод отвечал за построение Url (например, когда мы вызывали метод ActionLink: @Html.ActionLink("link", "Index")).

А в методе RouteAsync - мы обрабатываем запрос и записываем результат обработки в Response.

Следующая строка метода Configure:

routeBuilder.MapRoute("default", "{controller}/{action}/{id}");

Как две капли воды, похожа на использование метода MapRoute в MVC 5, его параметры - название добавляемого маршрута и шаблон с которым будет сопоставляться запрашиваемый Url.

Сам MapRoute() также как и в MVC 5 - extension-метод, а его вызов в итоге сводится к Созданию экземпляра класса TemplateRoute и добавлению его в коллекцию Routes нашего объекта RouteBuilder:

routeBuilder.Routes.Add(new TemplateRoute(routeCollectionBuilder.DefaultHandler,
    name, // в нашем случае передается "default"
    template, // в нашем случае передается "{controller}/{action}/{id}"
    ObjectToDictionary(defaults),
    ObjectToDictionary(constraints),
    ObjectToDictionary(dataTokens),
    inlineConstraintResolver));

Что интересно свойство Routes - это коллекция IRouter, то есть TemplateRoute тоже реализует интерфейс IRouter, как и созданный нами ASPNET5RoutingHandler, кстати, он передается в конструктор TemplateRoute.

И наконец последняя строчка:

app.UseRouter(routeBuilder.Build());

Вызов routeBuilder.Build() - создает экземпляр класса RouteCollection и добавляет в него все элементы из свойства Route класса RouteBuilder.

А app.UseRouter() - оказывается extension-методом, который на самом деле, подключает RouterMiddleware в pipeline обработки запроса, передавая ему созданный и заполненный в методе Build() объект RouteCollection.

public static IApplicationBuilder UseRouter([NotNull] this IApplicationBuilder builder, [NotNull] IRouter router)
{
    return builder.UseMiddleware<RouterMiddleware>(router);
}

И судя по конструктору RouterMiddleware:

public RouterMiddleware(
    RequestDelegate next,
    ILoggerFactory loggerFactory,
    IRouter router)

Объект RouteCollection тоже реализует интерфейс IRouter, как и ASPNET5RoutingHandler c TemplateRoute.

Итого у нас получилась следующая матрешка:

Наш обработчик запроса ASPNET5RoutingHandler упакован в TemplateRoute, сам TemplateRoute или несколько экземпляров TemplateRoute (если бы мы несколько раз вызвали метод MapRoute()) упакованы в RouteCollection, а RouteCollection передан в конструктор RouterMiddleware и сохранен в нем.

На этом процесс настройки системы маршрутизации завершен, можно запустить проект, перейти по адресу: “/Home/Index/1” и увидеть результат: “ASPNET5RoutingHandler work”.

Ну и кратко пройдемся по тому, что происходит, с системой маршрутизации во время входящего запроса:

Когда очередь доходит до RouterMiddleware, в списке запускаемых middleware, оно вызывает метод RouteAsync() у сохраненного экземпляра IRouter - это объект класса RouteCollection.

RouteCollection в свою очередь проходит по сохраненным в нем экземплярам IRouter - в нашем случае это будет TemplateRoute и вызывает у них метод RouteAsync().

TemplateRoute проверяет соответствует ли запрашиваемый Url, его шаблону (передавали в конструкторе TemplateRoute: “{controller}/{action}/{id}”) и если совпадает, вызывает хранящийся в нем экземпляр IRouter - которым является наш ASPNET5RoutingHandler.

##Подключаем систему маршрутизации к MVC приложению##

Теперь давайте посмотрим как связывается MVC Framework с системой маршрутизации.

Снова создадим пустой проект ASP.NET 5 используя Empty шаблон.

  1. Добавляем в зависимости (“dependencies”) проекта в файле project.json “Microsoft.AspNet.Mvc”:
"dependencies": {
    "Microsoft.AspNet.Server.IIS": "1.0.0-beta5",
    "Microsoft.AspNet.Server.WebListener": "1.0.0-beta5",
    "Microsoft.AspNet.Mvc": "6.0.0-beta5"
},
  1. В файле Startup.cs добавляем использование пространства имен Microsoft.AspNet.Builder:
using Microsoft.AspNet.Builder;

Нужные нам extensions-методы для подключения MVC находятся в нем.

  1. Добавляем сервисы, которые использует в своей работе MVC Framework: в методе ConfigureServices() файла Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();
}
  1. Настраиваем MVC приложение в методе Configure() файла Startup.cs:

Нам доступны три разных метода:

1.

public void Configure(IApplicationBuilder app)
{
    app.UseMvc()
}

2.

public void Configure(IApplicationBuilder app)
{
    app.UseMvcWithDefaultRoute()
}

3.

public void Configure(IApplicationBuilder app)
{
    return app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

Давайте сразу посмотрим реализацию этих методов:

Первый метод:

public static IApplicationBuilder UseMvc(this IApplicationBuilder app)
{
    return app.UseMvc(routes =>
    {
    });
}

Вызывает третий метод, передавая делегат Action<IRouteBuilder> который ничего не делает.

Второй метод:

public static IApplicationBuilder UseMvcWithDefaultRoute(this IApplicationBuilder app)
{
    return app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller=Home}/{action=Index}/{id?}");
    });
}

Тоже вызывает третий метод, только в делегате Action<IRouteBuilder> добавляется дефолтный маршрут.

Третий метод:

public static IApplicationBuilder UseMvc(
    this IApplicationBuilder app,
    Action<IRouteBuilder> configureRoutes)
{
    MvcServicesHelper.ThrowIfMvcNotRegistered(app.ApplicationServices);

    var routes = new RouteBuilder
    {
        DefaultHandler = new MvcRouteHandler(),
        ServiceProvider = app.ApplicationServices
    };

    configureRoutes(routes);

    // Adding the attribute route comes after running the user-code because
    // we want to respect any changes to the DefaultHandler.
    routes.Routes.Insert(0, AttributeRouting.CreateAttributeMegaRoute(
        routes.DefaultHandler,
        app.ApplicationServices));

    return app.UseRouter(routes.Build());
}

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

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

Convention-Based маршрутизация

Как я уже писал выше - настраивается с помощью вызова метода MapRoute() - и процесс использования этого метода не изменился со времен MVC 5 - в метод MapRoute() мы можем передать имя маршрута, его шаблон, значения по-умолчанию и ограничения.

routeBuilder.MapRoute("regexStringRoute", //name
    "api/rconstraint/{controller}", //template
    new { foo = "Bar" }, //defaults
    new { controller = new RegexRouteConstraint("^(my.*)$") }); //constraints

Attribute-Based маршрутизация

В отличии от MVC 5, где маршрутизацию с помощью атрибутов, нужно было специально включать, в MVC 6 она включена по-умолчанию.

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

Для задания маршрута нужно использовать атрибут Route как у методов действий, так и у контроллера (в MVC 5 для задания маршрута у контроллера использовался атрибут RoutePrefix).

[Route("appointments")]
public class Appointments : ApplicationBaseController
{
    [Route("check")]
    public IActionResult Index()
    {
        return new ContentResult
        {
            Content = "2 appointments available."
        };
    }
}

В итоге данный метод действия будет доступен по адресу: “/appointments/check”.

Настройка системы маршрутизации

В ASP.NET 5 появился новый механизм настройки сервисов называющийся Options - GitHub проекта. Он позволяет произвести некоторые настройки системы маршрутизации.

Смысл его работы, сводится к тому, что при настройке приложения в файле Startup.cs мы передаем в систему регистрации зависимостей некий объект, с заданными определенным образом свойствами, а во время работы приложения этот объект достается и в зависимости от значений выставленных свойств приложение строит свою работу.

Для настройки системы маршрутизации используется класс RouteOptions.

Для удобства нам доступен метод расширения ConfigureRouting:

public void ConfigureServices(IServiceCollection services)
{
    services.ConfigureRouting(
        routeOptions =>
        {
            routeOptions.LowercaseUrls = true; // генерация url в нижнем регистре
            routeOptions.AppendTrailingSlash = true; // добавление слеша в конец url
        });
}

“За кулисами” он просто делает вызов метода Configure передавая в него делегат Action<RouteOptions>:

public static void ConfigureRouting(
    this IServiceCollection services,
    Action<RouteOptions> setupAction)
{
    if (setupAction == null)
    {
        throw new ArgumentNullException(nameof(setupAction));
    }

    services.Configure(setupAction);
}

Шаблон маршрута

Принципы работы с шаблоном маршрута остались теми же, что были и в MVC 5:

  • Сегменты адреса разделены слешем: firstSegment/secondSegment.
  • Константной часть сегмента считается, если она не окаймлена фигурными скобками, соответствие такого маршрута запрашиваемому Url, происходит только если в адресе присутствуют точно такие же значения: firstSegment/secondSegment - такой маршрут соответствует только адресу вида: siteDomain/firstSegment/secondSegment.
  • Переменные части сегмента берутся в фигурные скобки: firstSegment/{secondSegment} - шаблон будет соответствовать любым двух сегментным адресам, где первый сегмент: “firstSegment”, а второй сегмент может быть любым набором символов (кроме слеша - так как это будет обозначать начало третьего сегмента):

    “/firstSegment/index”

    “/firstSegment/index-2”

  • Ограничения для переменной части сегмента, как это следует из названия - ограничивают допустимые значения переменного сегмента и задаются после символа “:”. На одну переменную часть можно наложить несколько ограничений, параметры передаются с использованием круглых скобок: firstSegment/{secondSegment:minlength(1):maxlength(3)}. Строковое обозначение ограничений можно посмотреть в методе GetDefaultConstraintMap() класса RouteOptions.
  • Для того, чтобы сделать последний сегмент “жадным”, так что он будет поглощать всю оставшуюся строку адреса, нужно использовать символ *: {controller}/{action}/{*allRest} - будет соответствовать как адресу: “/home/index/2”, так и адресу: “/home/index/2/4/5”.

Но в ASP.NET 5 шаблон маршрута получил некоторые дополнительные возможности:

  1. Возможность задавать прямо в нем значения по-умолчанию для переменных частей маршрута: {controller=Home}/{action=Index}.
  2. Задавать не обязательность переменной части сегмента с помощью символа ?: {controller=Home}/{action=Index}/{id?}.

Также при использовании шаблона маршрута в атрибутах, произошли изменения:

При настройке маршрутизации через атрибуты, к параметрам обозначающим контроллер и метод действия, теперь следует обращаться беря их в квадратные скобки и используя слова “controller” и “action”: “[controller]/[action]” - и использовать их можно только в таком виде - не разрешаются ни значения по-умолчанию, ни ограничения, ни опциональность, ни жадность.

То есть, разрешается:

Route("[controller]/[action]/{id?}")
Route("[controller]/[action]")

Можно использовать их по отдельности:

Route("[controller]")
Route("[action]")

Не разрешаются:

Route("{controller}/{action}")
Route("[controller=Home]/[action]")
Route("[controller?]/[action]")
Route("[controller]/[*action]")

Общая схема шаблона маршрута выглядит так:

constantPart-{variablePart}/{paramName:constraint1:constraint2=DefaultValue?}/{*lastGreedySegment}

Заключение:

В этой статье, мы пробежались по системе маршрутизации ASP.NET 5, посмотрели как она организована и подключили ее к пустому проекту ASP.NET 5, используя свой обработчик маршрута. Разобрали способы ее подключения к MVC приложению и настройке с помощью механизма Опций. Остановились на изменениях, которые произошли в использовании Attribute-Based маршрутизации и шаблоне маршрута.

Written on August 15, 2015