ASP.NET MVC 模糊操作方法

Posted

技术标签:

【中文标题】ASP.NET MVC 模糊操作方法【英文标题】:ASP.NET MVC ambiguous action methods 【发布时间】:2010-11-05 22:18:39 【问题描述】:

我有两个相互冲突的操作方法。基本上,我希望能够使用两条不同的路线到达同一个视图,或者通过项目的 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 参数可以为空,但我想不出解决它的最佳方法。我的设计一开始就很差吗?我考虑过扩展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)  ... 

【问题讨论】:

太棒了!小改动建议:(imo 真的很有用)1)params string[] valueNames 使属性声明更简洁(偏好)2)用return ValueNames.All(v => controllerContext.RequestContext.RouteData.Values.ContainsKey(v)); 替换 IsValidForRequest 方法体 嗨,乔恩,我想我不明白,因为 RouteData 中的查询参数在哪里? 我遇到了同样的查询字符串参数问题。如果您需要根据要求考虑的这些参数,请将contains = ... 部分换成如下内容:contains = controllerContext.RequestContext.RouteData.Values.ContainsKey(value) || controllerContext.RequestContext.HttpContext.Request.Params.AllKeys.Contains(value); 警告说明:所需的参数必须完全按名称发送。如果您的操作方法参数是通过按名称传递其属性来填充的复杂类型(并让 MVC 将它们按摩到复杂类型中),则此系统将失败,因为该名称不在查询字符串键中。例如,这将不起作用:ActionResult DoSomething(Person p),其中Person 具有各种简单的属性,如Name,并且直接使用属性名称(例如,/dosomething/?name=joe+someone&other=properties)向它发出请求。 如果你使用 MVC4 以后的版本,你应该使用controllerContext.HttpContext.Request[value] != null 而不是controllerContext.RequestContext.RouteData.Values.ContainsKey(value);不过还是不错的作品。 【参考方案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) 中包含的信息进行过滤。

【讨论】:

不错!我还没有看到 RequireRequestValue 属性。很高兴知道这一点。 我们可以使用 valueprovider 从多个来源获取值,例如:controllerContext.Controller.ValueProvider.GetValue(value); 我改为使用...RouteData.Values,但这“有效”。这是否是一个好的模式还有待商榷。 :) 我之前的编辑被拒绝了,所以我只想评论:[AttributeUsage(AttributeTargets.All, AllowMultiple=true)]【参考方案2】:

您的路由 roleIdapplicationNameroleName 中的参数与您的操作方法中的参数名称不匹配。我不知道这是否重要,但这会让你更难弄清楚你的意图是什么。

您的 itemId 是否符合可以通过正则表达式匹配的模式?如果是这样,那么您可以为您的路由添加一个限制,以便只有与该模式匹配的 url 被识别为包含 itemId。

如果您的 itemId 仅包含数字,那么这将起作用:

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

编辑:您还可以向AssignRemovePretty 路由添加一个约束,以便parentNameitemName 都是必需的。

编辑 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)  ... 

然后在路由中指定 Action 名称以强制调用正确的方法:

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】:

最近我借此机会改进了@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
    

有关更多信息和实施示例,请查看我在此主题上撰写的 this blog post。

【讨论】:

谢谢,进步很大!但 ParameterNames 未在 ctor 中设置【参考方案5】:
routes.MapRoute("AssignRemove",
                "Items/parentName/itemName",
                new  controller = "Items", action = "Assign" 
                );

考虑使用 MVC Contribs 测试路由库来测试您的路由

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

【讨论】:

以上是关于ASP.NET MVC 模糊操作方法的主要内容,如果未能解决你的问题,请参考以下文章

使用不记名令牌授权 ASP.net mvc 操作方法

在 asp.net-mvc 中,在不影响其他用户的情况下进行昂贵操作的正确方法是啥?

从 ASP.NET MVC 操作发送 HTTP 404 响应的正确方法是啥?

asp.net mvc 4 贝宝集成

如何在 ASP.NET Core MVC 中读取操作方法的属性?

ASP.NET MVC过滤器