Неожиданный маршрут, выбранный при генерации исходящего URL-адреса - программирование

Неожиданный маршрут, выбранный при генерации исходящего URL-адреса

Пожалуйста, рассмотрите следующие маршруты:

routes.MapRoute(
    "route1",
    "{controller}/{month}-{year}/{action}/{user}"
);
routes.MapRoute(
    "route2",
     "{controller}/{month}-{year}/{action}"
);

И следующие тесты:

ТЕСТ 1

[TestMethod]
public void Test1()
{
    RouteCollection routes = new RouteCollection();
    MvcApplication.RegisterRoutes(routes);
    RequestContext context = new RequestContext(CreateHttpContext(), 
                                                new RouteData());

    DateTime now = DateTime.Now;
    string result;

    context.RouteData.Values.Add("controller", "Home");
    context.RouteData.Values.Add("action", "Index");
    context.RouteData.Values.Add("user", "user1");
    result = UrlHelper.GenerateUrl(null, "Index", null,
                                    new RouteValueDictionary(
                                        new
                                        {
                                            month = now.Month,
                                            year = now.Year
                                        }),
                                    routes, context, true);
    //OK, result == /Home/10-2012/Index/user1
    Assert.AreEqual(string.Format("/Home/{0}-{1}/Index/user1", now.Month, now.Year), 
                    result);
}

ТЕСТ 2

[TestMethod]
public void Test2()
{
    RouteCollection routes = new RouteCollection();
    MvcApplication.RegisterRoutes(routes);
    RequestContext context = new RequestContext(CreateHttpContext(), 
                                                new RouteData());

    DateTime now = DateTime.Now;
    string result;

    context.RouteData.Values.Add("controller", "Home");
    context.RouteData.Values.Add("action", "Index");
    context.RouteData.Values.Add("user", "user1");
    context.RouteData.Values.Add("month", now.Month + 1);
    context.RouteData.Values.Add("year", now.Year);
    result = UrlHelper.GenerateUrl(null, "Index", null,
                                    new RouteValueDictionary(
                                        new
                                        {
                                            month = now.Month,
                                            year = now.Year
                                        }),
                                    routes, context, true);
    //Error because result == /Home/10-2012/Index
    Assert.AreEqual(string.Format("/Home/{0}-{1}/Index/user1", now.Month, now.Year), 
    result);
}

Этот тест эмулирует ситуацию, когда у меня уже есть значения маршрута в контексте запроса и вы пытаетесь создать исходящий URL с UrlHelper.

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

Итак, тест 1 работает хорошо, поскольку контекст запроса уже имеет значения для 3 из 5 сегментов маршрута 1, а значения для отсутствующих двух сегментов (а именно, year и month) передаются через routeValues параметр.

Тест 2 имеет значения для всех 5 сегментов в контексте запроса. И я хочу заменить некоторые из них (а именно месяц и год) другими значениями через t routeValues. Но маршрут 1, по-видимому, не подходит, и используется маршрут 2.

Почему? Что не соответствует моим маршрутам?

Как я ожидал, чтобы очистить контекст запроса вручную при таких обстоятельствах?

ИЗМЕНИТЬ

[TestMethod]
public void Test3()
{
    RouteCollection routes = new RouteCollection();
    MvcApplication.RegisterRoutes(routes);
    RequestContext context = new RequestContext(CreateHttpContext(), 
                                                new RouteData());

    DateTime now = DateTime.Now;
    string result;

    context.RouteData.Values.Add("controller", "Home");
    context.RouteData.Values.Add("action", "Index");
    context.RouteData.Values.Add("month", now.Month.ToString());
    context.RouteData.Values.Add("year", now.Year.ToString());
    result = UrlHelper.GenerateUrl(null, "Index", null,
                                    new RouteValueDictionary(
                                        new
                                        {
                                            month = now.Month + 1,
                                            year = now.Year + 1
                                        }),
                                    routes, context, true);
    Assert.AreEqual(string.Format("/Home/{0}-{1}/Index", now.Month + 1, now.Year + 1), 
                    result);
}

Этот тест усложняет ситуацию. Здесь я тестирую route2. И это работает! У меня есть значения для всех 4 сегментов в контексте запроса, передайте другие значения через routeValues, а сгенерированный исходящий URL-адрес в порядке.

Итак, проблема связана с route1. Что мне не хватает?

ИЗМЕНИТЬ

От Сандерсона С. Фримана А. - Про ASP.NET MVC 3 Framework Третье издание:

Система маршрутизации обрабатывает маршруты в том порядке, в котором они были добавлен в объект RouteCollection, переданный в RegisterRoutes метод. Каждый маршрут проверяется, будет ли он соответствовать, требует трех условий:

  • Значение должно быть доступно для каждой переменной сегмента, определенной в шаблоне URL. Чтобы найти значения для каждой переменной сегмента, маршрутизация система сначала смотрит на значения , которые мы предоставили (используя свойства анонимного типа), то значения переменных для текущий запрос и, наконец, значения по умолчанию, определенные в маршрут.
  • Ни одно из значений, которые мы предоставили для переменных сегмента, может не совпадать с переменными по умолчанию, определенными в маршруте. я не имеют значений по умолчанию в этих маршрутах.
  • Значения для всех переменных сегмента должны удовлетворять ограничениям маршрута. У меня нет ограничений в этих маршрутах.

Итак, согласно первому правилу, я указал значения в анонимном типе, у меня нет значений по умолчанию. Значения переменных для текущего запроса. Я полагаю, что это значения из контекста запроса.

Что не так с этими рассуждениями для route2, пока они хорошо работают для route1?

ИЗМЕНИТЬ

На самом деле все началось не с модульных тестов, а из реального приложения mvc. Там я использовал UrlHelper.Action Method (String, Object) для генерации исходящих URL-адресов. Поскольку этот метод используется в представлении макета (родительский для большинства всех представлений), я включил его в мой вспомогательный метод расширения (чтобы исключить дополнительную логику из представлений). Этот метод расширения извлекает имя действия из запроса контекст, переданный ему в качестве аргумента. Я знаю, что могу извлечь все текущие значения маршрута через контекст запроса и заменить те год и месяц (или я могу создать коллекцию значений анонимного маршрута, содержащую все значения из контекста), но я думал, что это было излишним, поскольку mvc автоматически учитывали значения, содержащиеся в контексте запроса. Таким образом, я извлек только имя действия, так как не было перегрузки UrlHelper.Action без имени действия (или мне бы даже понравилось "не указывать" имя действия) и добавили новый месяц и год через объект анонимного значения маршрута.

Это метод расширения:

public static MvcHtmlString GetPeriodLink(this HtmlHelper html, 
                                          RequestContext context, 
                                          DateTime date)
{
    UrlHelper urlHelper = new UrlHelper(context);
    return MvcHtmlString.Create(
              urlHelper.Action(
                 (string)context.RouteData.Values["action"], 
                 new { year = date.Year, month = date.Month }));
}

Как я описал в вышеприведенных тестах, он работал для более коротких маршрутов (когда контекст запроса содержал только контроллер, год и месяц и действие), но не удалось для более длинного (когда в контексте запроса содержался контроллер, год и месяц, действие, и пользователь).


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

Хотя я действительно хотел бы узнать, почему мне приходится делать какие-либо обходные пути в таком сценарии и каково ключевое различие между этими двумя маршрутами, что мешает route1 работать так, как это делает route2.


ИЗМЕНИТЬ

Еще одно замечание. Поскольку значения в контексте запроса имеют тип string, я решил попытаться установить их в контексте как строки, чтобы убедиться, что не было путаницы типа (int vs string). Я не понимаю, что изменилось в этом отношении, но некоторые из маршрутов начали генерировать правильно. Но не все... Это еще меньше смысла. Я изменил это в реальном приложении, а не на тестах, поскольку тесты имеют int в контексте, а не в строках.

Ну, я нашел условие, при котором используется маршрут1 - он используется только тогда, когда значения month и year в контексте равны значениям, указанным в анонимном объекте. Если они отличаются (в тестах это верно как для int, так и string), используется маршрут2. Но почему?

Это подтверждает то, что у меня есть в моем реальном приложении:

  • У меня был string в контексте, но при условии int через анонимный объект он каким-то образом смутил mvc, и он не мог использовать route1.
  • Я изменил int в анонимном объекте на string s, а URL-адреса, где month и year в контексте равны тем, которые находятся в анонимном объекте, начали генерировать правильно; тогда как все остальные не сделали.

Итак, я вижу одно правило: свойства анонимного объекта должны иметь тип string, чтобы соответствовать типу значений маршрута в контексте запроса.

Но это правило кажется не обязательным, как в Test3, я изменил типы (вы можете увидеть это сейчас выше), и он по-прежнему работает правильно. MVC правильно управляет типами.


Наконец, я нашел объяснение для всего этого поведения. Пожалуйста, см. Мой ответ ниже.

4b9b3361

Ответ 1

Это быстрое обходное решение, которое я использую для его работы:

public static MvcHtmlString GetPeriodLink(this HtmlHelper html, 
                                          RequestContext context, 
                                          DateTime date)
{
    UrlHelper urlHelper = new UrlHelper(context);

    context.RouteData.Values["month"] = date.Month;
    context.RouteData.Values["year"] = date.Year;

    return MvcHtmlString.Create(
              urlHelper.Action(
                 (string)context.RouteData.Values["action"]));
}

Я просто удаляю записи month и year из context.RouteData.Values. Я просто заменяю записи month и year в контексте запроса. Если удалить их из контекста (как я сделал сначала), они были бы недоступны для методов помощников, вызванных после этого.

Этот подход заставляет мой метод расширения работать по сценарию, описанному в тесте 1 (см. вопрос).


Finaly

Тщательно перечитывая Sanderson S., Freeman A. - Pro ASP.NET MVC 3 Framework (3-е издание) Я, по крайней мере, нашел объяснение всего этого:

Часть 2 ASP.NET MVC подробно

Глава 11 URL-адреса, маршрутизация и области

Создание исходящих URL-адресов

в разделе Общие сведения о повторном использовании переменных сегмента:

Система маршрутизации будет повторно использовать значения только для переменных сегмента, которые встречаются раньше в шаблоне URL, чем любые параметры, которые поставляются к методу Html.ActionLink.

Насколько мой сегмент month-year выполняется сразу после controller, и я указываю значения для month и year, все конечные сегменты (action, user) не используются повторно. Насколько я знаю и не указываю их в своем анонимном объекте, они, по-видимому, недоступны для маршрута. Таким образом, route1 не может совпадать.

В книге есть предупреждение:

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

Ну, это укусило меня)))

Стоит потерять 100 rep, чтобы помнить (я еще повторю его здесь) правило: Система маршрутизации будет повторно использовать значения только для переменных сегмента, которые встречаются ранее в шаблоне URL, чем любые параметры, которые поставляются.

Ответ 2

Достаточно ли одного маршрута со значением по умолчанию для пользователя? Это

routes.MapRoute(
    "route1",
    "{controller}/{month}-{year}/{action}/{user}",
    new { user = "" }
);

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

Обновление: я предлагаю оставить вашего помощника и работать напрямую с Url.Action, вот два теста для вашего сценария выше:

 [TestFixture]
    public class RouteRegistrarBespokeTests
    {
        private UrlHelper _urlHelper;

        [SetUp]
        public void SetUp()
        {
            var routes = new RouteCollection();
            routes.Clear();
            var routeData = new RouteData();
            RegisterRoutesTo(routes);
            var requestContext = new RequestContext(HttpMocks.HttpContext(), 
                                           routeData);
            _urlHelper = new UrlHelper(requestContext, routes);
        }

        [Test]
        public void Should_be_able_to_map_sample_without_user()
        {
            var now = DateTime.Now;
            var result = _urlHelper.Action("Index", "Sample", 
                               new {  year = now.Year, month = now.Month });
            Assert.AreEqual(string.Format("/Sample/{0}-{1}/Index", 
                                      now.Month, now.Year ), result);
        }

        [Test]
        public void Should_be_able_to_map_sample_with_user()
        {
            var now = DateTime.Now;
            var result = _urlHelper.Action("Index", "Sample", 
                          new { user = "user1", year = now.Year, 
                                month = now.Month });
            Assert.AreEqual(string.Format("/Sample/{0}-{1}/Index/{2}", 
                                     now.Month, now.Year, "user1"), result);
        }



private static void RegisterRoutesTo(RouteCollection routes)
{
    routes.MapRoute("route1", "{controller}/{month}-{year}/{action}/{user}");
    routes.MapRoute("route2", "{controller}/{month}-{year}/{action}");
}

}