ASP.NET MVC Routing Via Method Attributes

25,756

Solution 1

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.

Solution 2

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!

Solution 3

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

Solution 4

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.

Share:
25,756

Related videos on Youtube

TorgoGuy
Author by

TorgoGuy

Updated on July 05, 2022

Comments

  • TorgoGuy
    TorgoGuy almost 2 years

    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?

  • Nicolas Cadilhac
    Nicolas Cadilhac about 15 years
    I guess doing so with attributes prevents to use route defaults and route constraints...
  • DSO
    DSO about 15 years
    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).
  • Nicolas Cadilhac
    Nicolas Cadilhac about 15 years
    I meant defaults for the items in the route path like {username} but I also realize that they are usually primitives.
  • DSO
    DSO about 15 years
    Just updated with some additions I made to support route ordering, route parameter constraints and default values.
  • Nirvikalpa Samadhi
    Nirvikalpa Samadhi about 15 years
    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)
  • DSO
    DSO about 15 years
    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
    DSO about 15 years
    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.
  • Jarrod Dixon
    Jarrod Dixon about 15 years
    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.
  • BowserKingKoopa
    BowserKingKoopa over 14 years
    This is fantastic. I'm loving it.
  • Jarrod Dixon
    Jarrod Dixon over 14 years
    Very nice! I especially like the {?param} nomenclature for optional parameters.
  • Todd Menier
    Todd Menier almost 14 years
    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!
  • Erv Walter
    Erv Walter almost 14 years
    FYI, convention is to have the MVC related attributes on the ActionNameAsync method and not the ActionNameCompleted method.
  • TimDog
    TimDog almost 14 years
    Thanks -- did not realize that.
  • David Laing
    David Laing over 13 years
    This project seems more mature (better documentation, more feature, complete test suite) than the other options mentioned
  • Mike Chamberlain
    Mike Chamberlain almost 13 years
    I agree, this seems to do everything you could possibly want, and with good example documentation.
  • Valamas
    Valamas almost 13 years
    Thank you very much. I am happily using this solution and it has solved all my routing conflicts, ambiguity and confusion.
  • spot
    spot almost 13 years
    Your welcome very much. I love it too! :)
  • GONeale
    GONeale over 12 years
    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
    GONeale over 12 years
    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?
  • spot
    spot over 12 years
    @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!
  • Leniel Maccaferri
    Leniel Maccaferri over 12 years
    @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: github.com/mccalltd/AttributeRouting/issues/… you're the man!
  • Seba Illingworth
    Seba Illingworth about 12 years
    Its great, I use it lots - thanks @spot!