Metodi di azione ambigui ASP.NET MVC


135

Ho due metodi d'azione in conflitto. Fondamentalmente, voglio essere in grado di accedere alla stessa vista utilizzando due percorsi diversi, tramite l'ID di un articolo o il nome dell'articolo e i suoi genitori (gli articoli possono avere lo stesso nome tra genitori diversi). È possibile utilizzare un termine di ricerca per filtrare l'elenco.

Per esempio...

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

Ecco i miei metodi di azione (ci sono anche Removemetodi di azione) ...

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

Ed ecco i percorsi ...

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

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

Capisco perché si sta verificando l'errore, poiché il pageparametro può essere nullo, ma non riesco a capire il modo migliore per risolverlo. Il mio design è povero per cominciare? Ho pensato di estendere Method #1la firma per includere i parametri di ricerca e spostare la logica in Method #2un metodo privato che entrambi chiamerebbero, ma non credo che risolverà effettivamente l'ambiguità.

Qualsiasi aiuto sarebbe molto apprezzato.


Soluzione effettiva (basata sulla risposta di Levi)

Ho aggiunto la seguente classe ...

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

E poi decorato i metodi di azione ...

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

[RequireRouteValues(new[] { "itemId" })]
public ActionResult Assign(string itemId) { ... }

3
Grazie per aver pubblicato l'implementazione effettiva. Aiuta sicuramente le persone con problemi simili. Come ho fatto oggi. :-P
Paulo Santos,

4
Sorprendente! Piccolo suggerimento di modifica: (imo davvero utile) 1) params string [] valueNames per rendere più concisa la dichiarazione dell'attributo e (preferenza) 2) sostituire il corpo del metodo IsValidForRequest conreturn ValueNames.All(v => controllerContext.RequestContext.RouteData.Values.ContainsKey(v));
Benjamin Podszun

2
Ho avuto lo stesso problema con i parametri Querystring. Se hai bisogno di quei parametri considerati per il requisito, scambia la contains = ...sezione con qualcosa del genere:contains = controllerContext.RequestContext.RouteData.Values.ContainsKey(value) || controllerContext.RequestContext.HttpContext.Request.Params.AllKeys.Contains(value);
patridge

3
Nota di avviso al riguardo: i parametri richiesti devono essere inviati esattamente come indicato. Se il parametro del metodo di azione è un tipo complesso popolato passando le sue proprietà per nome (e lasciando che MVC li massaggi nel tipo complesso), questo sistema fallisce perché il nome non è nelle chiavi di querystring. Ad esempio, questo non funzionerà:, ActionResult DoSomething(Person p)dove Personha varie proprietà semplici come Name, e le richieste vengono fatte direttamente con i nomi delle proprietà (ad esempio, /dosomething/?name=joe+someone&other=properties).
Patridge,

4
Se stai usando MVC4 in poi, dovresti usare controllerContext.HttpContext.Request[value] != nullinvece di controllerContext.RequestContext.RouteData.Values.ContainsKey(value); ma comunque un bel lavoro.
Kevin Farrugia,

Risposte:


180

MVC non supporta il sovraccarico del metodo basato esclusivamente sulla firma, quindi questo non riuscirà:

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

Tuttavia, esso fa metodo supportano l'overload basato su attributi:

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

Nell'esempio sopra, l'attributo dice semplicemente "questo metodo corrisponde se la chiave xxx era presente nella richiesta." Puoi anche filtrare in base alle informazioni contenute nel percorso (controllerContext.RequestContext) se quello si adatta meglio ai tuoi scopi.


Questo finì per essere proprio quello di cui avevo bisogno. Come mi hai suggerito, dovevo usare controllerContext.RequestContext.
Jonathan Freeland,

4
Bello! Non avevo ancora visto l'attributo RequireRequestValue. È buono da sapere.
CoderDennis,

1
possiamo usare valueprovider per ottenere valori da diverse fonti come: controllerContext.Controller.ValueProvider.GetValue (value);
Jone Polvora,

Ho seguito ...RouteData.Valuesinvece il "ma" funziona. Che si tratti o meno di un buon modello è aperto al dibattito. :)
bambams,

1
Ho rifiutato la mia modifica precedente, quindi commenterò: [AttributeUsage (AttributeTargets.All, AllowMultiple = true)]
Mzn,

7

I parametri in tuoi percorsi {roleId}, {applicationName}e {roleName}non corrispondono ai nomi dei parametri nei tuoi metodi di azione. Non so se sia importante, ma rende più difficile capire quale sia la tua intenzione.

Il tuo oggettoId è conforme a un modello che potrebbe essere abbinato tramite regex? In tal caso, puoi aggiungere un sistema di ritenuta al tuo percorso in modo che solo gli URL che corrispondono al modello siano identificati come contenenti un itemId.

Se il tuo articolo contenesse solo cifre, questo funzionerebbe:

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

Modifica: è inoltre possibile aggiungere un vincolo al AssignRemovePrettypercorso in modo che entrambi {parentName}e {itemName}siano richiesti.

Modifica 2: Inoltre, poiché la tua prima azione è solo il reindirizzamento alla seconda azione, puoi rimuovere alcune ambiguità rinominando la prima.

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

Quindi specificare i nomi delle azioni nei percorsi per forzare il metodo corretto da chiamare:

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+" }
                );

1
Mi dispiace Dennis, i parametri in realtà corrispondono. Ho risolto la domanda. Proverò il controllo regex e ti ricontatterò. Grazie!
Jonathan Freeland,

La tua seconda modifica mi ha aiutato, ma alla fine è stato il suggerimento di Levi a sigillare l'affare. Grazie ancora!
Jonathan Freeland,


3

Di recente ho colto l'occasione per migliorare la risposta di @ Levi per supportare una gamma più ampia di scenari che ho dovuto affrontare, come ad esempio: supporto di più parametri, abbinarli a uno qualsiasi (anziché a tutti) e persino a nessuno di essi.

Ecco l'attributo che sto usando ora:

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

Per ulteriori informazioni e esempi di implementazione, consulta questo post sul blog che ho scritto su questo argomento.


Grazie, grande miglioramento! Ma ParameterNames non è impostato in ctor
nvirth

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

prendere in considerazione l'utilizzo della libreria dei percorsi di prova MVC Contribs per testare i percorsi

"Items/parentName/itemName".Route().ShouldMapTo<Items>(x => x.Assign("parentName", itemName));
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.