ASP.NET MVC Routing Via Method Attributes

In the StackOverflow Podcast #54, Jeff mentions they register their URL routes in the StackOverflow codebase via an attribute above the method that handles the route. Sounds like a good concept (with the caveat that Phil Haack brought up regarding route priorities).

Could someone provide some sample to make this happen?

Also, any "best practices" for using this style of routing?

25937 次浏览

UPDATE: This has been posted on codeplex. The complete source code as well as the pre-compiled assembly are there for download. I haven't had time to post the documentation on the site yet, so this SO post will have to suffice for now.

UPDATE: I added some new attributes to handle 1) route ordering, 2) route parameter constraints, and 3) route parameter default values. The text below reflects this update.

I've actually done something like this for my MVC projects (I have no idea how Jeff is doing it with stackoverflow). I defined a set of custom attributes: UrlRoute, UrlRouteParameterConstraint, UrlRouteParameterDefault. They can be attached to MVC controller action methods to cause routes, constraints, and defaults to be bound to them automatically.

Example usage:

(Note this example is somewhat contrived but it demonstrates the feature)

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

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

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

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

Changes to Global.asax.cs:

Replace calls to MapRoute, with a single call to the RouteUtility.RegisterUrlRoutesFromAttributes function:

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


RouteUtility.RegisterUrlRoutesFromAttributes(routes);
}

Definition of RouteUtility.RegisterUrlRoutesFromAttributes:

The full source is up on codeplex. Please go to the site if you have any feedback or bug reports.

This post is just to extend DSO's answer.

While converting my routes to attributes, I needed to handle the ActionName attribute. So in 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),
});

Also I found the naming of the route not suitable. The name is built dynamically with controllerName.RouteName. But my route names are const strings in the controller class and I use those const to also call Url.RouteUrl. That's why I really need the route name in the attribute to be the actual name of the route.

Another thing that I will do is to convert the default and constraint attributes to AttributeTargets.Parameter so that I can stick them to params.

1. Download RiaLibrary.Web.dll and reference it in your ASP.NET MVC website project

2. Decoreate controller methods with the [Url] Attributes:

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

BTW, '?' sign in '{?category}' parameter means that it's optional. You won't need to specify this explicitly in route defaults, which is equals to this:

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

3. Update Global.asax.cs file

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

How to set defaults and constraints? Example:

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

How to set ordering? Example:

[Url("forums/{?category}", Order = 2)]
public ActionResult Threads(string category)
{
return View();
}


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

I've combined these two approaches into a Frankensteinian version for anybody who wants it. (I liked the optional param notation, but also thought they should be separate attributes from default/constraints rather than all mixed into one).

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

I needed to get the ITCloud routing working in asp.net mvc 2 using an AsyncController -- to do so, just edit the RouteUtility.cs class in the source and recompile. You have to strip off the "Completed" from the action name on line 98

// 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,
});

Then, in the AsyncController, decorate the XXXXCompleted ActionResult with the familiar UrlRoute and UrlRouteParameterDefault attributes:

[UrlRoute(Path = "ActionName/{title}")]
[UrlRouteParameterDefault(Name = "title", Value = "latest-post")]
public ActionResult ActionNameCompleted(string title)
{
...
}

Hope that helps someone with the same issue.

You can also try AttributeRouting, which is available from github or via nuget.

This is a shameless plug, as I am the project author. But dang if I'm not very happy using it. You might be too. There is plenty of documentation and sample code in the github repository wiki.

With this library, you can do a lot:

  • Decorate your actions with GET, POST, PUT, and DELETE attributes.
  • Map multiple routes to a single action, ordering them with an Order property.
  • Specify route defaults and constraints using attributes.
  • Specify optional params with a simple ? token before the parameter name.
  • Specify the route name for supporting named routes.
  • Define MVC areas on a controller or base controller.
  • Group or nest your routes together using route prefixes applied to a controller or base controller.
  • Support legacy urls.
  • Set the precedence of routes among the routes defined for an action, within a controller, and among controllers and base controllers.
  • Generate lowercase outbound urls automatically.
  • Define your own custom route conventions and apply them on a controller to generate the routes for actions within the controller without boilerplate attributes (think RESTful style).
  • Debug your routes using a supplied HttpHandler.

I'm sure there's some other stuff I'm forgetting. Check it out. It's painless to install via nuget.

NOTE: As of 4/16/12, AttributeRouting also supports the new Web API infrastructure. Just in case you're looking for something that can handle that. Thanks subkamran!