ASP.NET MVCのあいまいなアクションメソッド


135

私には相反する2つの行動方法があります。基本的に、2つの異なるルートを使用して、アイテムのIDまたはアイテムの名前とその親のいずれか(アイテムは異なる親間で同じ名前を持つことができます)を使用して、同じビューにアクセスできるようにしたいと考えています。検索語を使用して、リストをフィルタリングできます。

例えば...

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パラメータがnullになる可能性があるため、エラーが発生する理由を理解していますが、それを解決する最良の方法を見つけることができません。そもそも私のデザインは貧弱ですか?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));
Benjamin Podszun

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)ます。それでも素晴らしい作品です。
Kevin Farrugia、2015

回答:


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 2009年

1
valueproviderを使用して、次のような複数のソースから値を取得できます。controllerContext.Controller.ValueProvider.GetValue(value);
Jone Polvora 2013

私は...RouteData.Values代わりに行ったが、これは「うまくいく」。それが良いパターンかどうかは、議論の余地があります。:)
バンバム

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:また、最初のアクションは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+" }
                );

1
申し訳ありませんが、デニス、パラメータは実際には一致しています。質問を修正しました。正規表現による制限を試して、ご連絡いたします。ありがとう!
ジョナサンフリーランド

あなたの2回目の編集は私を助けましたが、最終的には契約を締結したのはリーバイスの提案でした。再度、感謝します!
ジョナサンフリーランド


3

最近私は、@ Leviの回答を改善して、対処する必要があった幅広いシナリオをサポートする機会を得ました。たとえば、複数のパラメーターのサポート、(すべてではなく)それらのいずれかに一致し、いずれにも一致しない場合さえあります。

これが私が今使っている属性です:

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

詳細と実装方法のサンプルについては、このトピックについて私が書いたこのブログ投稿を確認してください。


ありがとう、大幅な改善!ただし、ParameterNamesは
ctorに

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));
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.