通过方法属性实现ASP.NET MVC路由

80
StackOverflow Podcast #54中,Jeff提到他们通过在处理路由的方法上方使用属性来注册其URL路由。听起来是一个不错的概念(但Phil Haack提出了有关路由优先级的警告)。
有人能提供一些示例来实现这一点吗?
此外,有没有使用这种路由风格的“最佳实践”?
6个回答

62

更新:这已经发布在codeplex上了。完整的源代码以及预先编译的程序集都可以下载。我还没有时间在网站上发布文档,所以现在只能用这篇SO帖子代替。

更新:我添加了一些新特性来处理1)路由排序、2)路由参数约束和3)路由参数默认值。下面的文本反映了此更新。

实际上,我为我的MVC项目做过类似的事情(我不知道Jeff在stackoverflow上是如何做到的)。我定义了一组自定义属性:UrlRoute、UrlRouteParameterConstraint、UrlRouteParameterDefault。它们可以附加到MVC控制器操作方法上,以便自动绑定路由、约束和默认值。

示例用法:

(请注意,此示例有些牵强,但它演示了该功能)

public class UsersController : Controller
{
    // Simple path.
    // Note you can have multiple UrlRoute attributes affixed to same method.
    [UrlRoute(Path = "users")]
    public ActionResult Index()
    {
        return View();
    }

    // Path with parameter plus constraint on parameter.
    // You can have multiple constraints.
    [UrlRoute(Path = "users/{userId}")]
    [UrlRouteParameterConstraint(Name = "userId", Regex = @"\d+")]
    public ActionResult UserProfile(int userId)
    {
        // ...code omitted

        return View();
    }

    // Path with Order specified, to ensure it is added before the previous
    // route.  Without this, the "users/admin" URL may match the previous
    // route before this route is even evaluated.
    [UrlRoute(Path = "users/admin", Order = -10)]
    public ActionResult AdminProfile()
    {
        // ...code omitted

        return View();
    }

    // Path with multiple parameters and default value for the last
    // parameter if its not specified.
    [UrlRoute(Path = "users/{userId}/posts/{dateRange}")]
    [UrlRouteParameterConstraint(Name = "userId", Regex = @"\d+")]
    [UrlRouteParameterDefault(Name = "dateRange", Value = "all")]
    public ActionResult UserPostsByTag(int userId, string dateRange)
    {
        // ...code omitted

        return View();
    }

UrlRouteAttribute的定义:

/// <summary>
/// Assigns a URL route to an MVC Controller class method.
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class UrlRouteAttribute : Attribute
{
    /// <summary>
    /// Optional name of the route.  If not specified, the route name will
    /// be set to [controller name].[action name].
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// Path of the URL route.  This is relative to the root of the web site.
    /// Do not append a "/" prefix.  Specify empty string for the root page.
    /// </summary>
    public string Path { get; set; }

    /// <summary>
    /// Optional order in which to add the route (default is 0).  Routes
    /// with lower order values will be added before those with higher.
    /// Routes that have the same order value will be added in undefined
    /// order with respect to each other.
    /// </summary>
    public int Order { get; set; }
}

UrlRouteParameterConstraintAttribute的定义:

/// <summary>
/// Assigns a constraint to a route parameter in a UrlRouteAttribute.
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class UrlRouteParameterConstraintAttribute : Attribute
{
    /// <summary>
    /// Name of the route parameter on which to apply the constraint.
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// Regular expression constraint to test on the route parameter value
    /// in the URL.
    /// </summary>
    public string Regex { get; set; }
}

UrlRouteParameterDefaultAttribute的定义:

/// <summary>
/// Assigns a default value to a route parameter in a UrlRouteAttribute
/// if not specified in the URL.
/// </summary>
[AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class UrlRouteParameterDefaultAttribute : Attribute
{
    /// <summary>
    /// Name of the route parameter for which to supply the default value.
    /// </summary>
    public string Name { get; set; }

    /// <summary>
    /// Default value to set on the route parameter if not specified in the URL.
    /// </summary>
    public object Value { get; set; }
}

修改Global.asax.cs:

将对MapRoute的调用替换为对RouteUtility.RegisterUrlRoutesFromAttributes函数的单个调用:

    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        RouteUtility.RegisterUrlRoutesFromAttributes(routes);
    }

RouteUtility.RegisterUrlRoutesFromAttributes的定义:

完整源代码可在codeplex上找到。如果您有任何反馈或错误报告,请访问该网站。


通过这种方法,我从来没有需要默认路由,因为你将每个路由绑定到特定的方法。你关于约束的观点是正确的。我研究了能否将约束添加为属性属性,但遇到了一个问题,即MVC约束使用匿名对象指定,而属性属性只能是简单类型。我认为仍然有可能将约束作为属性(需要更多编码),但我还没有去做,因为在我的MVC工作中到目前为止我还没有真正需要约束(我倾向于在控制器中验证路由值)。 - dso
干得好,谢谢。在我的一边,我也在试验它。UrlRouteParameterConstraintAttribute 的一个版本也可以采取 IRouteConstraint 类型的类型,但当然这个自定义限制只能用常量初始化,而不能使用实时变量。 - Nicolas Cadilhac
3
非常好!我们的RouteAttribute与此非常相似,只是添加了一些额外的辅助功能。我将添加一个详细说明差异的答案。 - Jarrod Dixon
1
这真是太棒了。我非常喜欢它。 - BowserKingKoopa
1
太好了!我已经使用MvcContrib有一段时间了,但不知道这个功能在其中。你在原始帖子中提到你没有时间记录它。现在还是这样吗?似乎在MvcContrib文档中至少提到它会非常有帮助,这样开发人员至少知道它的存在。谢谢! - Todd Menier
显示剩余5条评论

44

你还可以尝试使用AttributeRouting,可以通过github或者nuget获得。

我是这个项目的作者,这也是一种无耻的自夸。但是我真的很喜欢它,你也可能会喜欢。GitHub仓库wiki中有大量文档和示例代码。

使用这个库,你可以做很多事情:

  • 使用GET、POST、PUT和DELETE属性装饰你的操作。
  • 将多个路由映射到单个操作,并使用Order属性对它们进行排序。
  • 使用属性指定路由默认值和约束。
  • 使用简单的?标记在参数名称前指定可选参数。
  • 为支持命名路由指定路由名称。
  • 在控制器或基本控制器上定义MVC区域。
  • 使用应用于控制器或基本控制器的路由前缀将路由分组或嵌套。
  • 支持旧版URL。
  • 在操作、控制器内以及控制器和基本控制器之间定义路由的优先级。
  • 自动生成小写的出站URL。
  • 定义自己的自定义路由约定,并将其应用于控制器,以便生成控制器内操作的路由而不需要样板属性(类似RESTful风格)。
  • 使用提供的HttpHandler调试你的路由。

我确定还有其他东西我忘了提到。检查一下吧,通过nuget安装非常简便。

注意:截至2012年4月16日,AttributeRouting也支持新的Web API基础架构。 如果你正在寻找可以处理它的东西,只是为了确保。感谢subkamran


10
这个项目似乎比其他提到的选择更成熟(文档更好、功能更多、测试套件完整)。 - David Laing
3
我同意,这似乎可以满足你可能想要的所有功能,并且提供了良好的示例文档。 - Mike Chamberlain
3
非常感谢。我很高兴地使用这个解决方案,并且它解决了我所有的路由冲突、歧义和困惑。 - Valamas
3
嗨,Spot,你应该将上面的要点写在你的Github页面上,因为当我正在寻找更多细节时,我发现了这个Stack Overflow帖子 :) - GONeale
2
只是为了提出反对意见,将所有路由声明在一个地方有什么好处吗?我们在切换到这种方法时会失去什么,或者受到任何限制吗? - GONeale
显示剩余4条评论

9

1. 下载RiaLibrary.Web.dll并将其引用到您的ASP.NET MVC网站项目中

2. 使用[Url]属性装饰控制器方法:

public SiteController : Controller
{
    [Url("")]
    public ActionResult Home()
    {
        return View();
    }

    [Url("about")]
    public ActionResult AboutUs()
    {
        return View();
    }

    [Url("store/{?category}")]
    public ActionResult Products(string category = null)
    {
        return View();
    }
}

顺便提一下,在'{?category}'参数中的'?'符号表示它是可选的。在路由默认值中,您不需要明确指定这一点,因为它等同于以下内容:

routes.MapRoute("Store", "store/{category}",
new { controller = "Store", action = "Home", category = UrlParameter.Optional });

3. 更新 Global.asax.cs 文件

public class MvcApplication : System.Web.HttpApplication
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoutes(); // This does the trick
    }

    protected void Application_Start()
    {
        RegisterRoutes(RouteTable.Routes);
    }
}

如何设置默认值和约束条件?例如:
public SiteController : Controller
{
    [Url("admin/articles/edit/{id}", Constraints = @"id=\d+")]
    public ActionResult ArticlesEdit(int id)
    {
        return View();
    }

    [Url("articles/{category}/{date}_{title}", Constraints =
         "date=(19|20)\d\d-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])")]
    public ActionResult Article(string category, DateTime date, string title)
    {
        return View();
    }
}

如何设置排序?例如:
[Url("forums/{?category}", Order = 2)]
public ActionResult Threads(string category)
{
    return View();
}

[Url("forums/new", Order = 1)]
public ActionResult NewThread()
{
    return View();
}

1
非常好!我特别喜欢 {?param} 这种可选参数的命名方式。 - Jarrod Dixon

3

这篇文章是为了补充DSO的回答。

当我将我的路由转换成属性时,我需要处理ActionName属性。因此在GetRouteParamsFromAttribute中:

ActionNameAttribute anAttr = methodInfo.GetCustomAttributes(typeof(ActionNameAttribute), false)
    .Cast<ActionNameAttribute>()
    .SingleOrDefault();

// Add to list of routes.
routeParams.Add(new MapRouteParams()
{
    RouteName = routeAttrib.Name,
    Path = routeAttrib.Path,
    ControllerName = controllerName,
    ActionName = (anAttr != null ? anAttr.Name : methodInfo.Name),
    Order = routeAttrib.Order,
    Constraints = GetConstraints(methodInfo),
    Defaults = GetDefaults(methodInfo),
});

我发现路由的命名不太合适。名称是通过controllerName.RouteName动态构建的。但我的路由名称是控制器类中的const字符串,我还使用这些const来调用Url.RouteUrl。因此,我真的需要属性中的路由名称是实际路由的名称。

另一件事情是将默认值和约束属性转换为AttributeTargets.Parameter,以便我可以将它们粘贴到params上。


是的,我在路径命名行为上有些摇摆不定。最好像你所做的那样,只使用属性中的原样内容或将其设为空。在参数本身上设置默认值/约束的想法很好。我可能会在某个时候在 CodePlex 上发布这个来更好地管理更改。 - dso

0

我需要在使用AsyncController的asp.net mvc 2中使ITCloud路由工作 - 为此,只需编辑源代码中的RouteUtility.cs类并重新编译即可。您必须从第98行的操作名称中去掉“Completed”

// Add to list of routes.
routeParams.Add(new MapRouteParams()
{
    RouteName = String.IsNullOrEmpty(routeAttrib.Name) ? null : routeAttrib.Name,
    Path = routeAttrib.Path,
    ControllerName = controllerName,
    ActionName = methodInfo.Name.Replace("Completed", ""),
    Order = routeAttrib.Order,
    Constraints = GetConstraints(methodInfo),
    Defaults = GetDefaults(methodInfo),
    ControllerNamespace = controllerClass.Namespace,
});

然后,在 AsyncController 中,使用熟悉的 UrlRouteUrlRouteParameterDefault 属性装饰 XXXXCompleted ActionResult:
[UrlRoute(Path = "ActionName/{title}")]
[UrlRouteParameterDefault(Name = "title", Value = "latest-post")]
public ActionResult ActionNameCompleted(string title)
{
    ...
}

希望这能帮助到有同样问题的人。

顺便提一下,惯例是将MVC相关属性放在ActionNameAsync方法上,而不是ActionNameCompleted方法上。 - Erv Walter

0
我将这两种方法结合起来,形成了一个弗兰肯斯坦式的版本,适用于任何需要的人。(我喜欢可选参数标记,但也认为它们应该是与默认值/约束条件分开的属性,而不是全部混在一起。)

http://github.com/djMax/AlienForce/tree/master/Utilities/Web/


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接