Métodos de acción ambiguos ASP.NET MVC

135

Tengo dos métodos de acción que son conflictivos. Básicamente, quiero poder llegar a la misma vista usando dos rutas diferentes, ya sea por la ID de un elemento o por el nombre del elemento y el de su elemento primario (los elementos pueden tener el mismo nombre en diferentes elementos primarios). Se puede usar un término de búsqueda para filtrar la lista.

Por ejemplo...

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

Aquí están mis métodos de acción (también hay Removemétodos de acción) ...

// 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) { ... }

Y aquí están las rutas ...

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

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

Entiendo por qué se produce el error, ya que el pageparámetro puede ser nulo, pero no puedo encontrar la mejor manera de resolverlo. ¿Es mi diseño pobre para empezar? He pensado en extender Method #1la firma para incluir los parámetros de búsqueda y trasladar la lógica Method #2a un método privado que ambos llamarían, pero no creo que eso realmente resuelva la ambigüedad.

Cualquier ayuda sería muy apreciada.


Solución real (basada en la respuesta de Levi)

Agregué la siguiente clase ...

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; }
}

Y luego decoró los métodos de acción ...

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

[RequireRouteValues(new[] { "itemId" })]
public ActionResult Assign(string itemId) { ... }
Jonathan Freeland
fuente
3
Gracias por publicar la implementación real. Seguro que ayuda a las personas con problemas similares. Como lo hice hoy. :-P
Paulo Santos
44
¡Asombroso! Sugerencia de cambio menor: (imo realmente útil) 1) cadena de parámetros [] valueNames para hacer que la declaración de atributo sea más concisa y (preferencia) 2) reemplace el cuerpo del método IsValidForRequest conreturn ValueNames.All(v => controllerContext.RequestContext.RouteData.Values.ContainsKey(v));
Benjamin Podszun
2
Tuve el mismo problema con el parámetro querystring. Si necesita esos parámetros considerados para el requisito, cambie la contains = ...sección por algo como esto:contains = controllerContext.RequestContext.RouteData.Values.ContainsKey(value) || controllerContext.RequestContext.HttpContext.Request.Params.AllKeys.Contains(value);
patridge
3
Nota de advertencia sobre eso: los parámetros requeridos deben enviarse exactamente como se indica. Si el parámetro del método de acción es un tipo complejo poblado al pasar sus propiedades por nombre (y dejar que MVC las agregue al tipo complejo), este sistema falla porque el nombre no está en las claves de la cadena de consulta. Por ejemplo, esto no funcionará:, ActionResult DoSomething(Person p)donde Persontiene varias propiedades simples como Name, y las solicitudes se realizan con nombres de propiedad directamente (por ejemplo, /dosomething/?name=joe+someone&other=properties).
patridge
44
Si está utilizando MVC4 en adelante, debe usarlo en controllerContext.HttpContext.Request[value] != nulllugar de controllerContext.RequestContext.RouteData.Values.ContainsKey(value); pero un buen trabajo de todos modos.
Kevin Farrugia

Respuestas:

180

MVC no admite la sobrecarga de métodos basada únicamente en la firma, por lo que esto fallará:

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

Sin embargo, se hace método de soporte sobrecarga basado en el atributo:

[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; }
}

En el ejemplo anterior, el atributo simplemente dice "este método coincide si la clave xxx estaba presente en la solicitud". También puede filtrar por información contenida en la ruta (controllerContext.RequestContext) si eso se adapta mejor a sus propósitos.

Levi
fuente
Esto terminó siendo justo lo que necesitaba. Como sugirió, necesitaba usar controllerContext.RequestContext.
Jonathan Freeland
44
¡Agradable! Todavía no había visto el atributo RequireRequestValue. Es bueno saberlo.
CoderDennis
1
podemos usar valueprovider para obtener valores de varias fuentes como: controllerContext.Controller.ValueProvider.GetValue (value);
Jone Polvora
Fui después de la ...RouteData.Valuesen su lugar, pero esto "funciona". Si es o no un buen patrón está abierto a debate. :)
bambams
1
Obtuve mi edición anterior rechazada, así que solo voy a comentar: [AttributeUsage (AttributeTargets.All, AllowMultiple = true)]
Mzn
7

Los parámetros en sus rutas {roleId}, {applicationName}y {roleName}no coinciden con los nombres de los parámetros en sus métodos de acción. No sé si eso importa, pero hace que sea más difícil descubrir cuál es tu intención.

¿Su itemId se ajusta a un patrón que podría coincidir a través de regex? Si es así, puede agregar una restricción a su ruta para que solo las URL que coincidan con el patrón se identifiquen como que contengan un itemId.

Si su itemId solo contuviera dígitos, entonces esto funcionaría:

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

Editar: También puede agregar una restricción a la AssignRemovePrettyruta para que ambos {parentName}y {itemName}sean obligatorios.

Edición 2: Además, dado que su primera acción es simplemente redirigir a su segunda acción, puede eliminar cierta ambigüedad cambiando el nombre de la primera.

// 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) { ... }

Luego, especifique los nombres de Acción en sus rutas para forzar que se llame al método apropiado:

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+" }
                );
CodificadorDennis
fuente
1
Lo siento Dennis, los parámetros realmente coinciden. He arreglado la pregunta. Probaré la restricción regex y me pondré en contacto contigo. ¡Gracias!
Jonathan Freeland
Tu segunda edición me ayudó, pero finalmente fue la sugerencia de Levi lo que selló el trato. ¡Gracias de nuevo!
Jonathan Freeland
3

Recientemente aproveché la oportunidad de mejorar la respuesta de @ Levi para admitir una gama más amplia de escenarios con los que tuve que lidiar, como: compatibilidad con múltiples parámetros, igualar cualquiera de ellos (en lugar de todos) e incluso igualar ninguno de ellos.

Aquí está el atributo que estoy usando ahora:

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

Para más información y cómo-a muestras de implementación echa un vistazo a esta entrada del blog que escribí sobre este tema.

Darkseal
fuente
Gracias, gran mejora! Pero ParameterNames no está configurado en ctor
nvirth
0
routes.MapRoute("AssignRemove",
                "Items/{parentName}/{itemName}",
                new { controller = "Items", action = "Assign" }
                );

considere usar la biblioteca de rutas de prueba de MVC Contribs para probar sus rutas

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