AttributeRouting

Define your routes directly on actions and controllers in:

ASP.NETMVC
ASP.NET/Self-Hosted
Web API

Getting Started

ASP.NET MVC

Install the AttributeRouting nuget package:

PM> Install-Package AttributeRouting

The nuget package drops a file in your project at ~/App_Start/AttributeRouting.cs/vb. This file configures AttributeRouting to scan your project's assembly for route attributes when your app starts.

Hooray! At this point, you are ready to start using AttributeRouting! Jump to the basics →.

ASP.NET Web API

Install the AttributeRouting.WebApi nuget package:

PM> Install-Package AttributeRouting.WebApi

The nuget package drops a file in your project at ~/App_Start/AttributeRoutingHttp.cs/vb. This file configures AttributeRouting to scan your project's assembly for route attributes when your app starts.

Beware! Due to integration issues with the Web API WebHost framework, the following features will not work:

  • performance enhancements when matching routes,
  • custom route handlers,
  • querystring parameter constraints,
  • subdomain routing,
  • localization applied to inbound/outbound urls, and
  • lowercasing, appending prefixes, etc to generated routes.

These features all have to wait for vNext of the Web API.

Self-Hosted Web API

Install the AttributeRouting.WebApi.Hosted nuget package:

PM> Install-Package AttributeRouting.WebApi.Hosted

The install drops a file in your project at ~/AttributeRouting.cs/vb. This file contains a static method for registering routes in the HttpSelfHostConfiguration.

Here's a quick example of setting up a self-hosted application:

class Program
{
    static void Main(string[] args)
    {
        var config = new HttpSelfHostConfiguration("http://localhost:8080");

        config.Routes.MapHttpAttributeRoutes(cfg => 
        {
            cfg.AddRoutesFromAssembly(Assembly.GetExecutingAssembly());
            // Specify other configuration options here.
        });

        using (var server = new HttpSelfHostServer(config))
        {
            server.OpenAsync().Wait();

            Console.WriteLine("Routes:");

            config.Routes.Cast<HttpRoute>().LogTo(Console.Out);

            Console.WriteLine("Press Enter to quit.");
            Console.ReadLine();
        }
    }
}

Basics

Defining Routes

Routes are defined directly on actions using attributes. There are five such attributes: GET, which generates a URL that responds to GET and HEAD requests; POST, which generates a URL that responds to POST requests; PUT, which generates a URL that responds to PUT requests; DELETE, which generates a URL that responds to DELETE requests; and Route, which generates a URL that responds to all or only specified methods.

using AttributeRouting.Web.Http;
				
public class SampleController : Controller
{
    [GET("Sample")]
    public ActionResult Index() { /* ... */ }
                    
    [POST("Sample")]
    public ActionResult Create() { /* ... */ }
                    
    [PUT("Sample/{id}")]
    public ActionResult Update(int id) { /* ... */ }
                    
    [DELETE("Sample/{id}")]
    public string Destroy(int id) { /* ... */ }
    
    [Route("Sample/Any-Method-Will-Do")]
    public string Wildman() { /* ... */ }
}

Attention VB.NET users! Get is a restricted keyword in the language, so to specify a GET request, you must enter: [[GET]("Some/Url")].

Multiple Routes on an Action

You can have multiple GET routes on a single action. If you do so, be sure to use the ActionPrecedence property to indicate the primary route so that you generate the correct outbound URLs.

[GET("", ActionPrecedence = 1)]
[GET("Posts")]
[GET("Posts/Index")]
public ActionResult Index() { /* ... */ }

Route Defaults

Default values for URL parameters are specified inline. Separate the parameter from the default value with an equals sign:

[GET("Demographics/{state=MT}/{city=Missoula}")]
public ActionResult Index(string state, string city) { /* ... */ }

Optional Route Parameters

Optional URL parameters are specified inline. Append a question mark after the parameter name:

[GET("Demographics/{state?}/{city?}")]
public ActionResult Index(string state, string city) { /* ... */ }

Route Constraints

Route constraints are specified inline. They take the form {parameterName:constraint(params)}. Following are all the built-in constraints:

// Type constraints
[GET("Int/{x:int}")]
[GET("Long/{x:long}")]
[GET("Float/{x:float}")]
[GET("Double/{x:double}")]
[GET("Decimal/{x:decimal}")]
[GET("Bool/{x:bool}")]
[GET("Guid/{x:guid}")]
[GET("DateTime/{x:datetime}")]
                    
// String constraints
[GET("Alpha/{x:alpha}")]
[GET("Length/{x:length(1)}")]
[GET("LengthRange/{x:length(2, 10)}")]
[GET("MinLength/{x:minlength(10)}")]
[GET("MaxLength/{x:maxlength(10)}")]

// Numeric constraints
[GET("Min/{x:min(1)}")]
[GET("Max/{x:max(10)}")]
[GET("Range/{x:range(1, 10)}")]
                    
// Regex constraint
[GET(@"Regex/{x:regex(^Howdy$)}")]
                    
// Querystring parameter constraints
[GET("QuerystringParamExists?{x}")]
[GET("QuerystringParamsExist?{x}&{y}")]
[GET("QuerystringParamConstraints?{x:int}&{y:int}")]

Chaining and Combining with Defaults and Optionals

You can chain constraints, effectively and’ing them, and you can constrain while also specifying default values and optional params:

[GET("Chained/{state:alpha:length(2)}")] 
[GET("Defaults/{state:alpha:length(2)=MT}")]
[GET("Optionals/{age:min(18)?}")]

Adding Custom Constraints

The constraint system is plug-and-play. You can easily add your own IRouteConstraint or IHttpRouteConstraint when configuring AttributeRouting using the InlineRouteConstraints property on the configuration object.

routes.MapAttributeRoutes(cfg =>
{
    // ...
    cfg.InlineRouteConstraints.Add("custom", typeof(CustomConstraint));
});

Then:

[GET("Path/{param:custom}")] 

Simple Enum Constraints

There is a generic attribute EnumRouteConstraint<T>, which allows you to register your own enum constraints via the extensibility method described above.

routes.MapAttributeRoutes(cfg =>
{
    // ...
    cfg.InlineRouteConstraints.Add("color", typeof(EnumRouteConstraint<Color>));
});

Then:

[GET("Paintbrush/{which:color}")] 

Constraining URL Parameters Globally

You can configure constraints to apply to parameters globally across all the routes you define using the AddDefaultRouteConstraint method on the configuration object. This works by matching route parameters names against a specified pattern. When a match is found, then the specified constraint is applied against the parameter.

routes.MapAttributeRoutes(cfg =>
{
    // ...
    cfg.AddDefaultRouteConstraint("id", new IntRouteConstraint());
});

Named Routes

To use named routes, specify a value for the RouteName property of the route attributes.

[GET("Named/Route", RouteName = "Awesome")])

Auto-Generating Route Names

You can have AttributeRouting generate route names for you automatically. Just configure this feature when registering attribute routes:

routes.MapAttributeRoutes(config =>
{
    // ...
    config.AutoGenerateRouteNames = true;
    config.RouteNameBuilder = RouteNameBuilders.FirstInWins;
});

The RouteNameBuilder property is a Func<RouteSpecification, string> delegate. You can define your own delegate to name your routes, or use one of the two RouteNameBuilders provided by AttributeRouting:

Heads up! For ASP.NET MVC and ASP.NET Web API projects, the default builder is FirstInWins. For Self-hosted Web API projects, the default builder is Unique, due to the fact that self-hosted Web API applications require every route to be uniquely named.

Route Prefixes

You can prefix all the routes in a controller using the RoutePrefix attribute. This is handy when you want to nest routes.

[RoutePrefix("Posts/{postId}")]
public class CommentsController : ControllerBase    
{
    [GET("Comments")]
    public ActionResult Index(int postId) { /* ... */ }

    [GET("Comments/{id}")]
    public ActionResult Show(int postId, int id) { /* ... */ }
}

This will generate the following routes:

Multiple Route Prefixes on a Controller

If you want to apply multiple route prefixes, go right ahead:

[RoutePrefix("Prefix/First", Precedence = 1)]
[RoutePrefix("Prefix/Second")]
public class MultiplePrefixController : Controller
{
    [GET("Index")]
    public ActionResult Index() { /* ... */ }
    
    [GET("Details")]
    public ActionResult Details() { /* ... */ }
}

This will generate the following routes:

Notice the Precedence property on that first prefix. It controls the order in which multiple prefixes are applied. The value is used in the same way as the other precedence properties (more here).

Ignoring Route Prefixes for Certain Routes

If you want to ignore the route prefix for a given route, just say so:

[RouteArea("Area")]
[RoutePrefix("Prefix")]
public class IgnorePrefixController : Controller
{
    [GET("Index")] // => "Area/Prefix/Index"
    [GET("NoPrefix", IgnoreRotuePrefix = true)] // => "Area/NoPrefix"
    [GET("Absolute", IsAbsoluteUrl = true)] // => "Absolute"
    public ActionResult Index() { /* ... */ }
}

ASP.NET MVC Areas

Areas can be mapped by applying the RouteAreaAttribute on a controller. All the routes defined in the controller will be mapped to the specified area. These routes will also be prefixed with the area name.

[RouteArea("Admin")]
public class PostsController : ControllerBase { /* ... */ }

If you are defining more than one controller for an area, consider using a base controller decorated with the RouteAreaAttribute and deriving all the controllers in the area from the base controller.

[RouteArea("Admin")]
public abstract class AdminControllerBase : Controller { /* ... */ }

public class PostsController : AdminControllerBase { /* ... */ }
public class CommentsController : AdminControllerBase { /* ... */ }
public class TagsController : AdminControllerBase { /* ... */ }

Overriding the Area URL Prefix

By default, areas defined with the RouteAreaAttribute use the area name as a prefix for all routes in that area. To override this behavior, use the AreaUrl property of the RouteAreaAttribute:

[RouteArea("AreaName", AreaUrl = "MyCustomPrefix")]

Ignoring Area URLs for Certain Routes

If you want to ignore the area URL prefix for a given route, just say so:

[RouteArea("Area")]
[RoutePrefix("Prefix")]
public class IgnorePrefixController : Controller
{
    [GET("Index")] // => "Area/Prefix/Index"
    [GET("NoAreaUrl", IgnoreAreaUrl = true)] // => "Prefix/NoAreaUrl"
    [GET("Absolute", IsAbsoluteUrl = true)] // => "Absolute"
    public ActionResult Index() { /* ... */ }
}

Configuration

Important Note

Once you start to customize the configuration settings, you must tell AttributeRouting the assemblies or controller types you wish to scan for route attributes. Luckily this is simple. Just use one of the following methods of the configuration object: AddRoutesFromAssembly, AddRoutesFromController, or AddRoutesFromControllersOfType:

routes.MapAttributeRoutes(config =>
{
    config.AddRoutesFromAssembly(Assembly.GetExecutingAssembly());
    config.AddRoutesFromController<MyController>();
    config.AddRoutesFromControllersOfType<MyBaseController>();
});

Route Precedence

There are four ways to control route precedence:

  1. among routes for an action using the ActionPrecedence property of the route attributes;
  2. among routes in a controller using the ControllerPrecedence property of the route attributes;
  3. among controllers in a site using AddRoutesFromController method of the configuration object;
  4. among routes in a site using the SitePrecedence property of the route attributes.

Controlling First and Last among Actions, Controllers, and Sites

As you read on, keep in mind that when using the ActionPrecedence, ControllerPrecedence, and SitePrecedence properties, you can specify the order using positive and negative integers, allowing you to control what routes are first and last:

  • (0), 1, 2, 3, ... = first, second, third, ...
  • -1, -2, -3, ... = last, second to last, third from last, ...

Precedence Among Routes for an Action

If you need to specify the precedence of routes defined against an action, you can use the ActionPrecedence property of the route attributes:

[GET("", ActionPrecedence = 1)]
[GET("Posts")]
[GET("Posts/Index")]
public void Index() { /* ... */ }

Precedence Among Routes in a Controller

By default, the order of a route among the routes defined for a controller is determined by the order of the action in the controller. If you need to override this behavior, use the ControllerPrecedence property of the route attributes:

public class PrecedenceController : Controller
{
    [GET("Route1", ControllerPrecedence = 1)]
    public ActionResult Route1() { /* ... */ }

    [GET("Route3")]
    public ActionResult Route3() { /* ... */ }

    [GET("Route2", ControllerPrecedence = 2)]
    public ActionResult Route2() { /* ... */ }
}

Precedence Among Controllers in a Site

To control the precedence of routes on a per-controller basis, use the AddRoutesFromController method of the configuration object:

routes.MapAttributeRoutes(config =>
{
    config.AddRoutesFromController<PostsController>();
    config.AddRoutesFromController<HomeController>();
});

You can also choose to add the routes from controllers to the beginning or end of the route table:

routes.MapAttributeRoutes(config =>
{
    config.AddRoutesFromController<PostsController>();
    config.AddRoutesFromAssembly(Assembly.GetExecutingAssembly());
    config.AddRoutesFromController<AccountController>();
});

In the preceding case, the routes from the PostController are registered first, then all the routes from the executing assembly are registered, and then the routes from the AccountController are registered.

There is another method, AddRoutesFromControllersOfType, which is useful if you want to register all the routes from an area, say, before the other routes in your application. This is convenient if you use a base class for an area.

routes.MapAttributeRoutes(config =>
{
    config.AddRoutesFromControllersOfType<AdminControllerBase>();
    config.AddRoutesFromAssembly(Assembly.GetExecutingAssembly());
});

Precedence Among Routes in a Site

If you need to take an arbitrary route and put it at the top or bottom of your route table, use the SitePrecedence property of the route attributes:

[GET("I-Am-The-First-Route", SitePrecedence = 1)]
public string IAmTheFirstRoute() { /* ... */ }

[GET("I-Am-The-Last-Route", SitePrecedence = -1)]
public string IAmTheLastRoute() { /* ... */ }

Lowercase URL Generation

If you want to generate lowercase outbound urls, you can set the UseLowercaseRoutes property on the configuration object:

routes.MapAttributeRoutes(config =>
{
    // ...
    config.UseLowercaseRoutes = true;
});

If you would like to preserve the case of URL parameters while lowercasing the rest of the url, use the PreserveCaseForUrlParameters property on the configuration object:

routes.MapAttributeRoutes(config =>
{
    // ...
    config.UseLowercaseRoutes = true;
    config.PreserveCaseForUrlParameters = true;
});

Overriding the Global Configuration Settings

If you want to override the global config settings for a single route, use the UseLowercaseRoute and PreserveCaseForUrlParameters properties of the route attributes:

[GET("Whatever", UseLowercaseRoute = true)]
[GET("Whatever", UseLowercaseRoute = true, PreserveCaseForUrlParameters = true)]

Trailing Slash URL Generation

If you want to generate outbound urls that end with a trailing slash, use the AppendTrailingSlash property of the configuration object:

routes.MapAttributeRoutes(config =>
{
    // ...
    config.AppendTrailingSlash = true;
});

Overriding the Global Configuration Setting

If you want to override the global config setting for a single route, use the AppendTrailingSlash property of the route attributes:

[GET("Whatever", AppendTrailingSlash = true)]

Advanced Features

Localizing URLs

You can localize your URLs using an AttribtueRouting translation provider. A translation provider can be used to work against databases, resx files, etc. To create your own, simply extend TranslationProviderBase:

public abstract class TranslationProviderBase
{
    /// <summary>
    /// List of culture names that have translations available via this provider.
    /// <summary>
    public abstract IEnumerable<string> CultureNames { get; }

    /// <summary>
    /// Gets the translation for the given route component by key and culture.
    /// <summary>
    /// <param name="key">The key of the route component to translate.<param>
    /// <param name="cultureName">The culture name for the translation.<param>
    public abstract string Translate(string key, string cultureName);
}

Then, register your provider via the AddTranslationProvider method of the configuration object.

routes.MapAttributeRoutes(config =>
{
    // ...
    config.AddTranslationProvider(new CustomTranslationProvider());
});

Using the Built-In FluentTranslationProvider

The FluentTranslationProvider stores translation in-memory in a dictionary.

You can add translations in a strongly-typed manner:

var provider = new FluentTranslationProvider();

// You can add translations in a strongly-typed manner
provider.AddTranslations()
    .ForController<TranslationController>()
    .AreaUrl(new Dictionary<string, string>
    {
        { "es", "es-Area" }
    })
    .RoutePrefixUrl(new Dictionary<string, string>
    {
        { "es", "es-Prefix" }
    })
    .RouteUrl(c => c.Index(), new Dictionary<string, string>
    {
        { "es", "es-Index" }
    });

You can also add translations by refencing the keys specified by the TranslationKey properties on the RouteArea, RoutePrefix, and GET, POST, PUT, DELETE and Route attributes.

var provider = new FluentTranslationProvider();

translations.AddTranslations()
    .ForKey("CustomAreaKey", new Dictionary<string, string>
    {
        { "es", "es-CustomArea" }
    })
    .ForKey("CustomPrefixKey", new Dictionary<string, string>
    {
        { "es", "es-CustomPrefix" }
    })
    .ForKey("CustomRouteKey", new Dictionary<string, string>
    {
        { "es", "es-CustomIndex" }
    });

If You Roll Your Own, Use Translation Keys

When using the FLuentTranslationProvider, you don't have to worry about translation keys, as they are managed internally and are based upon the names of your areas, controllers, and action methods. However, if you use your own translation provider, you will want to apply translation keys to the components of your routes. There is a TranslationKey property available on the RouteArea, RoutePrefix, and GET, POST, PUT, DELETE and Route attributes

[RouteArea("Area", TranslationKey = "CustomAreaKey")]
[RoutePrefix("Prefix", TranslationKey = "CustomPrefixKey")]
public class TranslationController : Controller
{
    [GET("Index", TranslationKey = "CustomRouteKey")]
    public ActionResult CustomIndex() { /* ... */ }
}

Translations and Inbound Requests

A route is added to the route table for each translation provided. So if you have 10 routes and translate the URLs for two cultures, you will have 30 routes in your route table.

By default, the inbound request handling doesn't care what about the current request culture, so if you are browsing in Spanish, requesting the English or French URLs for an action will also work. However, you can change this via the ConstrainTranslatedRoutesByCurrentUICulture property of the configuration object:

routes.MapAttributeRoutes(config =>
{
    // ...
    config.AddTranslationProvider(provider);
    config.ConstrainTranslatedRoutesByCurrentUICulture = true;
});

When you choose to constrain inbound requests this way, a route is considered when:

If you want to use URL parameters for specifying the culture (/en/home, /pt/inicio, etc), then use the CurrentUICultureResolver property of the configuration object. Given the current HTTP context and route data, this delegate returns the culture name. By default, it returns the name of the current UI culture for the current thread.

routes.MapAttributeRoutes(config =>
{
    // ...
    config.ConstrainTranslatedRoutesByCurrentUICulture = true;
    config.CurrentUICultureResolver = (httpContext, routeData) =>
    {
        return (string)routeData.Values["culture"]
               ?? Thread.CurrentThread.CurrentUICulture.Name;
    };
});

Translations and Outbound Routes

Translated URLs will be generated by the MVC framework via UrlHelper.Action() and Html.ActionLink() if a translation is available for the route. In order to support this, you must set the thread's CurrentUICulture property. A simple solution involves detecting the user's preferences via the request context:

// In global.asax
public MvcApplication()
{
    BeginRequest += OnBeginRequest;
}

protected void OnBeginRequest(object sender, System.EventArgs e)
{
    if (Request.UserLanguages != null && Request.UserLanguages.Any())
    {
        var cultureInfo = new CultureInfo(Request.UserLanguages[0]);
        Thread.CurrentThread.CurrentUICulture = cultureInfo;
    }
}

DRY: Route Conventions

You can define custom route conventions by applying an attribute derived from RouteConventionAttributeBase (source) to your controllers. It's very simple:

public class MyRouteConventionAttribute : RouteConventionAttributeBase
{
    public override IEnumerable<IRouteAttribute> 
                    GetRouteAttributes(MethodInfo actionMethod)
    {
        // TODO: Yield IRouteAttributes (GET/POST/PUT/DELETE/Route).
    }
}

[MyRouteConvention]
public class MyController : Controller { /* ... */ }

Take Heed! When defining your own convention, it will help to know a bit about how the routes are constructed and added to the route table:

  1. When the routes are being scanned, convention based routes for an action are registered before explicitly defined routes. So if you want your explicitly defined routes to come first, use the ActionPrecedence property.
  2. If you decide to override the virtual GetDefaultRoutePrefixes or GetDefaultRouteArea method, note that the attributes you generate will only be used if no explicit attributes are applied to your controller. Explicit attributes will act as overrides.

Two conventions are provided out-of-the-box. These provide example of what you can do.

Restful Route Convention

Use the RestfulRouteConventionAttribute like so:

[RestfulRouteConvention]
public class PostsController : Controller
{
    public ActionResult Index() { /* ... */ }

    public ActionResult New() { /* ... */ }

    public ActionResult Create() { /* ... */ }

    public ActionResult Show(int id) { /* ... */ }

    public ActionResult Edit(int id) { /* ... */ }

    public ActionResult Update(int id) { /* ... */ }

    public ActionResult Delete(int id) { /* ... */ }

    public ActionResult Destroy(int id) { /* ... */ }
}

This will add the following routes to the route table:

Action HTTP Method URL
IndexGET~/Posts
NewGET~/Posts/New
CreatePOST~/Posts
ShowGET~/Posts/{id}
EditGET~/Posts/{id}/Edit
UpdatePUT~/Posts/{id}
DeleteGET~/Posts/{id}/Delete
DestroyDELETE~/Posts/{id}

Default Http Route Convention

Use the DefaultHttpRouteConventionAttribute like so:

[DefaultHttpRouteConvention]
public class ProductsController : ApiController 
{
    public IEnumerable<string> GetAll() { /* ... */ }

    public string Get(int id) { /* ... */ }

    public string Post() { /* ... */ }

    public string Delete(int id) { /* ... */ }

    public string Put(int id) { /* ... */ }
}

This will add the following routes to the route table:

Action HTTP Method URL
GetAllGET~/Products
GetGET~/Products/{id}
PostPOST~/Products
PutPUT~/Products/{id}
DeleteDELETE~/Products/{id}

Subdomain Routing

You can map your ASP.NET MVC areas to subdomains using the Subdomain property of the RouteAreaAttribute. Doing so ensures that the routes for the area are matched only when requests are made to the specified subdomain. Here's how:

[RouteArea("Users", Subdomain = "users")]
public class SubdomainController : Controller
{
    [GET("")]
    public ActionResult Index() { /* ... */ }
}

When you do this, the area URL prefix ("Users" in this case) is not added to the route registered in the route table. So the index action will end up matching http://users.domain.com/, not http://users.domain.com/Users.

If you want to have an area map to a subdomain and have a URL prefix for the area, use the AreaUrl property like so:

[RouteArea("Admin", Subdomain = "internal", AreaUrl = "admin")]
public class SubdomainWithAreaUrlController : Controller
{
    [GET("")]
    public ActionResult Index() { /* ... */ }
}

The route registered in the route table in this case will match http://internal.domain.com/admin.

Configuring Subdomain Parsing Logic and the Default Subdomain

By default, AttributeRouting will treat everything from the requested hostname up to the domain name as the subdomain. This is based on an assumed format like {localName}.domain.com. AR also assumes that the default subdomain for an application is www, which is obviously going to be wrong in some cases. To remain flexible, you can configure AR to use a custom delegate for parsing the subdomain from the hostname. You can also configure the default subdomain name.

routes.MapAttributeRoutes(config =>
{
    // ...
    config.SubdomainParser = host => "return whatever string value you want";
    config.DefaultSubdomain = "xyz";
});

Dynamically Configuring Subdomains

If your subdomains change depending on the hosted environment, or if you need to configure the subdomains at runtime, use the MapArea method of the configuration object:

routes.MapAttributeRoutes(config =>
{
    // ...
    config.MapArea("AreaName").ToSubdomain("whatever");
});

More...

Debugging Routes

Use the LogRoutesHandler to emits the routes in the RouteTable to a browser. To use it, add the following line to your web.config:

<httpHandlers>
    <add path="routes.axd" verb="GET" 
         type="AttributeRouting.Web.Logging.LogRoutesHandler, AttributeRouting"/>
</httpHandlers>

Heads Up! If you installed via nuget, then this handler was registered automagically.

T4 Templates

There are controller templates available for both C# and Visual Basic for MVC 2, 3, and 4. They are available in the t4 folder off the project root.

If you're using MVC 4, you're in luck – the nuget package AttributeRouting.CodeTemplates.MVC4 will pull the templates into your project:

PM> Install-Package AttributeRouting.CodeTemplates.MVC4

Back to the Top