Красивые URL в приложениях ASP.NET MVC

Каждому разработчику на ASP.NET MVC известно, что платформа имеет мощную и удобную систему определения маршрутов в приложении. На основе этих маршрутов выбираются подходящие контроллеры и действия, а также генерируются адреса (URL) внтури приложения. Что меня всегда угнетало, это то, что имена контроллеров/действий используются генерируются в адресе "как есть". В итоге пользователь видит что-то уродливое вроде http://myapp.ru/Contacts/List. Рассмотрим способ как это можно исправить.

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

<a href="@Url.Action("List","Contacts")">Contacts</a>

Как по мне, так URL в нижнем регистре выглядят более привлекательно. Говорят, так же, что это важно для поисковой оптимизации (SEO).

Существует два способа решения проблемы — простой (доступный в .NET 4.5 и выше) и сложный (доступный до версии 4.5).

Решение проблемы в .NET 4.5

Решение указанной проблемы в .NET 4.5 лежит на поверхности. Для этого при определении маршрутов достаточно установить свойство LowercaseUrls в true у объекта RouteCollection. В дополнение в .NET 4.5 появилась опция AppendTrailingSlash, при помощи которой мы можем принудительно добавлять слеш в конце.

public static void RegisterRoutes(RouteCollection routes)
{
  routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

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

  routes.LowercaseUrls = true;
  routes.AppendTrailingSlash = true;
}

Решение проблемы для проект .NET версии ранее 4.5

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

Как известно, в коллекцию RouteCollection добавляются объекты типа Route, которые и содержат информацию о маршруте. Переопределив поведение этого объекта, можно влиять на то, как будут генерироваться URL. Создадим такой класс:

public class LowercaseRoute : System.Web.Routing.Route
{
  public LowercaseRoute(string url, IRouteHandler routeHandler)
      : base(url, routeHandler)
  {
  }

  public LowercaseRoute(string url, RouteValueDictionary defaults, IRouteHandler routeHandler)
      : base(url, defaults, routeHandler)
  {
  }

  public LowercaseRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints,
                                IRouteHandler routeHandler)
      : base(url, defaults, constraints, routeHandler)
  {
  }

  public LowercaseRoute(string url, RouteValueDictionary defaults, RouteValueDictionary constraints,
                                RouteValueDictionary dataTokens, IRouteHandler routeHandler)
      : base(url, defaults, constraints, dataTokens, routeHandler)
  {
  }

  public override VirtualPathData GetVirtualPath(RequestContext requestContext, RouteValueDictionary values)
  {
      VirtualPathData path = base.GetVirtualPath(requestContext, values);

      if (path != null)
            path.VirtualPath = path.VirtualPath.ToLowerInvariant();

      return path;
  }
}

Теперь можно добавить этот маршрут в коллекцию RouteCollection простым вызовом метода Add. Чтобы это можно было делать как и раньше (путем вызова метода MapRoute) создадим расширение для RouteCollection.

public static class LowercaseRouteExtensions
{
  public static Route MapRouteLowercase(this RouteCollection routes,
      string name, string url, object defaults)
  {
    var route = new LowercaseRoute(url,
          CreateRouteValueDictionary(defaults),
          new MvcRouteHandler());

    routes.Add(route);

    return route;
  }

  private static RouteValueDictionary CreateRouteValueDictionary(object values)
  {
    IDictionary<string object="" ,=""> dictionary = values as IDictionary<string object="" ,="">;

    if (dictionary != null)
    {
          return new RouteValueDictionary(dictionary);
    }

    return new RouteValueDictionary(values);
  }
}

Всё, что делает MapRouteLowercase - это создает новый объект-наследник Route и добавляет его в RouteCollection. Этот класс можно расширить и другими методами с различным количеством параметров. Это не трудно сделать, поэтому полный листинг здесь не привожу.

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

public static void RegisterRoutes(RouteCollection routes)
{
  routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

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

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