80

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?

Community
  • 1
  • 1
TorgoGuy
  • 1,316
  • 1
  • 9
  • 10

6 Answers6

62

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.

dso
  • 9,463
  • 10
  • 53
  • 59
  • I guess doing so with attributes prevents to use route defaults and route constraints... – Nicolas Cadilhac May 22 '09 at 13:39
  • With this approach I've never needed default routes because you're binding each route to a specific method. You are right about constraints. I looked into being able to add constraints as an attribute property, but ran into a snag in that MVC constraints are specified using anonymous objects and attribute properties can only be simple types. It is still possible I think to do constraints as an attribute (with more coding), but I haven't bothered with it yet because I haven't really needed constraints in my MVC work up to this point (I tend to validate route values in the controller). – dso May 22 '09 at 15:53
  • I meant defaults for the items in the route path like {username} but I also realize that they are usually primitives. – Nicolas Cadilhac May 22 '09 at 17:06
  • Just updated with some additions I made to support route ordering, route parameter constraints and default values. – dso May 23 '09 at 07:47
  • Well done, and thank you. On my side I was also playing with it. A version of the UrlRouteParameterConstraintAttribute could also take the type of a IRouteConstraint, but of course this custom constraint could not be initialized with live variables, only constants. – Nicolas Cadilhac May 23 '09 at 14:41
  • Has anyone run into problems run this in IIS6 / windows 2003? Runs fine on my localhost but testing it on my other machine it breaks! (cant debug as dont have VS installed on it) – David May 28 '09 at 20:21
  • I've been using this on a live web site running IIS6/Win2k3 without issues so far. I use the IIS wildcard mapping approach. – dso May 28 '09 at 20:37
  • 3
    Very nice! Our RouteAttribute is very similar to this, only adding a bit of additional helper functionality. I'll have to add an answer detailing the differences. – Jarrod Dixon Jun 04 '09 at 09:31
  • 1
    This is fantastic. I'm loving it. – BowserKingKoopa Jan 26 '10 at 05:44
  • 1
    This is great! I've been using MvcContrib for a while and had no idea this was in there. You mentioned in your original post that you didn't have time to document it. Is that still the case? Seems like at least a mention of it in the MvcContrib docs would be very helpful so that developers at least know it's there. Thanks! – Todd Menier Jul 08 '10 at 15:23
44

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!

Community
  • 1
  • 1
spot
  • 2,413
  • 2
  • 19
  • 16
  • 10
    This project seems more mature (better documentation, more feature, complete test suite) than the other options mentioned – David Laing Mar 09 '11 at 16:58
  • 3
    I agree, this seems to do everything you could possibly want, and with good example documentation. – Mike Chamberlain Jul 05 '11 at 06:36
  • 3
    Thank you very much. I am happily using this solution and it has solved all my routing conflicts, ambiguity and confusion. – Valamas Jul 06 '11 at 06:44
  • Your welcome very much. I love it too! :) – spot Jul 06 '11 at 20:49
  • 3
    Hey spot, you should write the above bullet points on your github page, as I found this SO post as I was on the hunt for more details :) – GONeale Sep 28 '11 at 00:48
  • 2
    Just to play devils advocate, is there any benefit to declaring your routes all in one place? Like do we lose anything, or are limited in any fashion switching to this method? – GONeale Sep 28 '11 at 00:54
  • 1
    @GONeal - Good idear; just did that. And I feel the benefit is the elimination of accidental route behavior due to explicit declarations, and keeping routes closer to actions makes it much easier to keep your routes at top of mind. But hey - different strokes for different folks! – spot Oct 14 '11 at 20:23
  • @spot After seeing this answer I started using your AWESOME lib. I got here looking for ways of translating route names... When I saw the Localization feature recently added (Feb/2012) I thought "this is exactly what I'm looking for"... :) Can you please advise if this is the correct way of implementing what I want: https://github.com/mccalltd/AttributeRouting/issues/20#issuecomment-4442500 you're the man! – Leniel Maccaferri Mar 11 '12 at 22:10
  • Its great, I use it lots - thanks @spot! – Seba Illingworth Apr 06 '12 at 08:58
9

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();
}
G S
  • 35,511
  • 22
  • 84
  • 118
Konstantin Tarkus
  • 37,618
  • 14
  • 135
  • 121
3

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.

Nicolas Cadilhac
  • 4,680
  • 5
  • 41
  • 62
  • Yeah I kind of oscillated on the route naming behavior. Its probably best to do what you did, just use whats in the attribute as-is or make it null. Nice idea putting the default/constraints on the params themselves. I'll probably post this on codeplex at some point to better manage changes. – dso May 28 '09 at 20:40
0

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/

Max Metral
  • 41
  • 2
0

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.

TimDog
  • 8,758
  • 5
  • 41
  • 50
  • FYI, convention is to have the MVC related attributes on the ActionNameAsync method and not the ActionNameCompleted method. – Erv Walter Jul 23 '10 at 13:28