ASP.NET MVC неоднозначные методы действия

135

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

Например...

Items/{action}/ParentName/ItemName
Items/{action}/1234-4321-1234-4321

Вот мои методы действия (есть также Removeметоды действия) ...

// Method #1
public ActionResult Assign(string parentName, string itemName) { 
    // Logic to retrieve item's ID here...
    string itemId = ...;
    return RedirectToAction("Assign", "Items", new { itemId });
}

// Method #2
public ActionResult Assign(string itemId, string searchTerm, int? page) { ... }

А вот и маршруты ...

routes.MapRoute("AssignRemove",
                "Items/{action}/{itemId}",
                new { controller = "Items" }
                );

routes.MapRoute("AssignRemovePretty",
                "Items/{action}/{parentName}/{itemName}",
                new { controller = "Items" }
                );

Я понимаю, почему возникает ошибка, так как pageпараметр может быть нулевым, но я не могу найти лучший способ ее устранения. Мой дизайн плох для начала? Я думал о расширении Method #1подписи для включения параметров поиска и переноса логики в Method #2частный метод, который они оба вызовут, но я не верю, что это на самом деле разрешит неоднозначность.

Любая помощь будет принята с благодарностью.


Актуальное решение (на основе ответа Леви)

Я добавил следующий класс ...

public class RequireRouteValuesAttribute : ActionMethodSelectorAttribute {
    public RequireRouteValuesAttribute(string[] valueNames) {
        ValueNames = valueNames;
    }

    public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) {
        bool contains = false;
        foreach (var value in ValueNames) {
            contains = controllerContext.RequestContext.RouteData.Values.ContainsKey(value);
            if (!contains) break;
        }
        return contains;
    }

    public string[] ValueNames { get; private set; }
}

А потом украсили методы действия ...

[RequireRouteValues(new[] { "parentName", "itemName" })]
public ActionResult Assign(string parentName, string itemName) { ... }

[RequireRouteValues(new[] { "itemId" })]
public ActionResult Assign(string itemId) { ... }
Джонатан Фриленд
источник
3
Спасибо за публикацию фактической реализации. Это, безусловно, помогает людям с похожими проблемами. Как и сегодня. :-P
Пауло Сантос
4
Удивительный! Незначительные изменения: (imo действительно полезно) 1) params string [] valueNames, чтобы сделать объявление атрибута более кратким и (предпочтение) 2) заменить тело метода IsValidForRequest наreturn ValueNames.All(v => controllerContext.RequestContext.RouteData.Values.ContainsKey(v));
Бенджамин Подсзун
2
У меня была та же проблема параметра строки запроса. Если вам нужны эти параметры, учитываемые в требовании, contains = ...contains = controllerContext.RequestContext.RouteData.Values.ContainsKey(value) || controllerContext.RequestContext.HttpContext.Request.Params.AllKeys.Contains(value);
замените
3
Обратите внимание на предупреждение об этом: обязательные параметры должны быть отправлены в точности как указано. Если ваш параметр метода действия представляет собой сложный тип, заполняемый путем передачи его свойств по имени (и позволяющий MVC втирать их в сложный тип), эта система завершается сбоем, поскольку имя отсутствует в ключах строки запроса. Например, это не будет работать:, ActionResult DoSomething(Person p)где Personимеет различные простые свойства, такие как Name, и запросы к нему делаются с именами свойств напрямую (например, /dosomething/?name=joe+someone&other=properties).
Патридж
4
Если вы используете MVC4 и выше, вы должны использовать controllerContext.HttpContext.Request[value] != nullвместо controllerContext.RequestContext.RouteData.Values.ContainsKey(value); но хорошая работа тем не менее.
Кевин Фарругия,

Ответы:

180

MVC не поддерживает перегрузку методов, основанную исключительно на сигнатуре, поэтому это не удастся:

public ActionResult MyMethod(int someInt) { /* ... */ }
public ActionResult MyMethod(string someString) { /* ... */ }

Тем не менее, он поддерживает перегрузку метода на основе атрибута:

[RequireRequestValue("someInt")]
public ActionResult MyMethod(int someInt) { /* ... */ }

[RequireRequestValue("someString")]
public ActionResult MyMethod(string someString) { /* ... */ }

public class RequireRequestValueAttribute : ActionMethodSelectorAttribute {
    public RequireRequestValueAttribute(string valueName) {
        ValueName = valueName;
    }
    public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo) {
        return (controllerContext.HttpContext.Request[ValueName] != null);
    }
    public string ValueName { get; private set; }
}

В приведенном выше примере атрибут просто говорит: «этот метод соответствует, если в запросе присутствовал ключ xxx ». Вы также можете фильтровать информацию, содержащуюся в маршруте (controllerContext.RequestContext), если это лучше соответствует вашим целям.

Леви
источник
Это оказалось именно тем, что мне было нужно. Как вы предложили, мне нужно было использовать controllerContext.RequestContext.
Джонатан Фриленд
4
Ницца! Я еще не видел атрибут RequireRequestValue. Это хорошо знать.
CoderDennis
1
мы можем использовать valueprovider для получения значений из нескольких источников, таких как: controllerContext.Controller.ValueProvider.GetValue (value);
Джоне Полвора
Я пошел ...RouteData.Valuesвместо этого, но это "работает". Является ли это хорошим шаблоном, открыто для обсуждения. :)
bambams
1
Моя предыдущая правка отклонена, поэтому я просто прокомментирую: [AttributeUsage (AttributeTargets.All, AllowMultiple = true)]
Mzn
7

Параметры в маршрутах {roleId}, {applicationName}и {roleName}не совпадают с именами параметров в своих методах действий. Я не знаю, имеет ли это значение, но становится сложнее выяснить, каково ваше намерение.

Соответствует ли ваш itemId шаблону, который можно сопоставить с помощью регулярных выражений? Если это так, то вы можете добавить ограничение к вашему маршруту, чтобы идентифицировать только URL-адреса, соответствующие шаблону, как itemId.

Если ваш itemId содержит только цифры, это будет работать:

routes.MapRoute("AssignRemove",
                "Items/{action}/{itemId}",
                new { controller = "Items" },
                new { itemId = "\d+" }
                );

Редактировать: Вы также можете добавить ограничение к AssignRemovePrettyмаршруту, так что оба {parentName}и {itemName}обязательны.

Изменить 2: Кроме того, так как ваше первое действие просто перенаправляет на ваше второе действие, вы можете устранить некоторую двусмысленность, переименовав первое.

// Method #1
public ActionResult AssignRemovePretty(string parentName, string itemName) { 
    // Logic to retrieve item's ID here...
    string itemId = ...;
    return RedirectToAction("Assign", itemId);
}

// Method #2
public ActionResult Assign(string itemId, string searchTerm, int? page) { ... }

Затем укажите имена действий в ваших маршрутах, чтобы вызвать соответствующий метод:

routes.MapRoute("AssignRemove",
                "Items/Assign/{itemId}",
                new { controller = "Items", action = "Assign" },
                new { itemId = "\d+" }
                );

routes.MapRoute("AssignRemovePretty",
                "Items/Assign/{parentName}/{itemName}",
                new { controller = "Items", action = "AssignRemovePretty" },
                new { parentName = "\w+", itemName = "\w+" }
                );
CoderDennis
источник
1
Извините, Деннис, параметры действительно совпадают. Я исправил вопрос. Я опробую ограничение регулярных выражений и вернусь к вам. Спасибо!
Джонатан Фриленд
Ваша вторая редакция помогла мне, но в конечном итоге это было предложение Леви, которое закрыло сделку. Еще раз спасибо!
Джонатан Фриленд
7

Другой подход - переименовать один из методов, чтобы не было конфликтов. Например

// GET: /Movies/Delete/5
public ActionResult Delete(int id = 0)

// POST: /Movies/Delete/5
[HttpPost, ActionName("Delete")]
public ActionResult DeleteConfirmed(int id = 0)

См. Http://www.asp.net/mvc/tutorials/getting-started-with-mvc3-part9-cs

RickAndMSFT
источник
3

Недавно я воспользовался возможностью улучшить ответ @ Levi's для поддержки более широкого диапазона сценариев, с которыми мне приходилось иметь дело, таких как: поддержка нескольких параметров, сопоставление любого из них (вместо всех) и даже совпадение ни с одним из них.

Вот атрибут, который я использую сейчас:

/// <summary>
/// Flags an Action Method valid for any incoming request only if all, any or none of the given HTTP parameter(s) are set,
/// enabling the use of multiple Action Methods with the same name (and different signatures) within the same MVC Controller.
/// </summary>
public class RequireParameterAttribute : ActionMethodSelectorAttribute
{
    public RequireParameterAttribute(string parameterName) : this(new[] { parameterName })
    {
    }

    public RequireParameterAttribute(params string[] parameterNames)
    {
        IncludeGET = true;
        IncludePOST = true;
        IncludeCookies = false;
        Mode = MatchMode.All;
    }

    public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
    {
        switch (Mode)
        {
            case MatchMode.All:
            default:
                return (
                    (IncludeGET && ParameterNames.All(p => controllerContext.HttpContext.Request.QueryString.AllKeys.Contains(p)))
                    || (IncludePOST && ParameterNames.All(p => controllerContext.HttpContext.Request.Form.AllKeys.Contains(p)))
                    || (IncludeCookies && ParameterNames.All(p => controllerContext.HttpContext.Request.Cookies.AllKeys.Contains(p)))
                    );
            case MatchMode.Any:
                return (
                    (IncludeGET && ParameterNames.Any(p => controllerContext.HttpContext.Request.QueryString.AllKeys.Contains(p)))
                    || (IncludePOST && ParameterNames.Any(p => controllerContext.HttpContext.Request.Form.AllKeys.Contains(p)))
                    || (IncludeCookies && ParameterNames.Any(p => controllerContext.HttpContext.Request.Cookies.AllKeys.Contains(p)))
                    );
            case MatchMode.None:
                return (
                    (!IncludeGET || !ParameterNames.Any(p => controllerContext.HttpContext.Request.QueryString.AllKeys.Contains(p)))
                    && (!IncludePOST || !ParameterNames.Any(p => controllerContext.HttpContext.Request.Form.AllKeys.Contains(p)))
                    && (!IncludeCookies || !ParameterNames.Any(p => controllerContext.HttpContext.Request.Cookies.AllKeys.Contains(p)))
                    );
        }
    }

    public string[] ParameterNames { get; private set; }

    /// <summary>
    /// Set it to TRUE to include GET (QueryStirng) parameters, FALSE to exclude them:
    /// default is TRUE.
    /// </summary>
    public bool IncludeGET { get; set; }

    /// <summary>
    /// Set it to TRUE to include POST (Form) parameters, FALSE to exclude them:
    /// default is TRUE.
    /// </summary>
    public bool IncludePOST { get; set; }

    /// <summary>
    /// Set it to TRUE to include parameters from Cookies, FALSE to exclude them:
    /// default is FALSE.
    /// </summary>
    public bool IncludeCookies { get; set; }

    /// <summary>
    /// Use MatchMode.All to invalidate the method unless all the given parameters are set (default).
    /// Use MatchMode.Any to invalidate the method unless any of the given parameters is set.
    /// Use MatchMode.None to invalidate the method unless none of the given parameters is set.
    /// </summary>
    public MatchMode Mode { get; set; }

    public enum MatchMode : int
    {
        All,
        Any,
        None
    }
}

Для получения дополнительной информации и примеров реализации ознакомьтесь с этой записью в блоге, которую я написал на эту тему.

Darkseal
источник
Спасибо, большое улучшение! Но ParameterNames не устанавливается в ctor
nvirth
0
routes.MapRoute("AssignRemove",
                "Items/{parentName}/{itemName}",
                new { controller = "Items", action = "Assign" }
                );

Рассмотрите возможность использования библиотеки тестовых маршрутов MVC Contribs для проверки ваших маршрутов.

"Items/parentName/itemName".Route().ShouldMapTo<Items>(x => x.Assign("parentName", itemName));
Rony
источник