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

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

Например...

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

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

// Method #1
public ActionResult Assign(string parentName, string itemName) { 
    // Logic to retrieve item 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) { ... }

Ответ 1

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), если это лучше подходит для ваших целей.

Ответ 2

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

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

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

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

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

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

// Method #1
public ActionResult AssignRemovePretty(string parentName, string itemName) { 
    // Logic to retrieve item 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+" }
                );

Ответ 3

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

// 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

Ответ 4

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

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

"Items/parentName/itemName".Route().ShouldMapTo<Items>(x => x.Assign("parentName", itemName));