ASP.NET MVC歧义操作方法


135

我有两种冲突的操作方法。基本上,我希望能够使用两条不同的路线(通过项的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
Paulo Santos

4
惊人!较小的更改建议:(imo确实有用)1)参数string [] valueNames以使属性声明更加简洁和(首选项)2)将IsValidForRequest方法主体替换为return ValueNames.All(v => controllerContext.RequestContext.RouteData.Values.ContainsKey(v));
Benjamin Podszun 2012年

2
我有同样的querystring参数问题。如果您需要考虑那些需要的参数,请将该contains = ...部分替换为以下内容:contains = controllerContext.RequestContext.RouteData.Values.ContainsKey(value) || controllerContext.RequestContext.HttpContext.Request.Params.AllKeys.Contains(value);
patridge

3
关于以下警告的提示:必需的参数必须完全按照命名的名称发送。如果您的操作方法参数是通过按名称传入其属性填充的复杂类型(并让MVC将其按摩为复杂类型),则该系统将失败,因为该名称不在查询字符串键中。例如,这将不起作用:ActionResult DoSomething(Person p),其中Person具有各种简单属性Name(例如/dosomething/?name=joe+someone&other=properties),并且直接使用属性名称(例如)进行请求。
patridge,2012年

4
如果您以后使用的是MVC4,则应使用controllerContext.HttpContext.Request[value] != null而不是controllerContext.RequestContext.RouteData.Values.ContainsKey(value); 但是仍然是一件很棒的工作。
凯文·法鲁吉亚

Answers:


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);
Jone Polvora

...RouteData.Values代替了,但是这个“有效”。是否是一个好的模式尚待争论。:)
bambams 2013年

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

1
抱歉,丹尼斯,参数确实匹配。我已经解决了这个问题。我将尝试使用正则表达式约束并与您联系。谢谢!
乔纳森·弗里兰德

您的第二次编辑帮了我忙,但最终是李维斯(Levi)的建议使交易达成了。再次感谢!
乔纳森·弗里兰德


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

有关更多信息和操作方法示例,请查看我撰写的有关此主题的博客文章


谢谢,有很大的进步!但是未在ctor中设置ParameterNames
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));
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.