Using Attribute Routing in ASP.NET MVC

Introduction

ASP.NET MVC maps a URL to an action method through what is known as Routing. By default an ASP.NET MVC URL includes a controller and an action name where the request finally lands. However, you can customize many aspects of a route such as root prefix and route parameters. ASP.NET MVC 5 allows you to define routes through certain attributes. This attribute routing is simpler and more intuitive than the older technique of defining routes because a route definition is closer to the controller and its action method. This article discusses how attribute routing can be used with examples.

An Example Scenario

Let’s assume that you are building a blog engine that stores and displays blog posts. Further, let’s suppose that the blog post data is stored in a database named BlogDb in a table BlogPosts. The ADO.NET entity data model for BlogPosts table is shown below:

The ADO.NET entity data model for BlogPosts table
The ADO.NET entity data model for BlogPosts table

The BlogPosts table contains only four columns namely PostID, Title, Content and PublishDate (of course, a real world blog engine would contain many other tables and columns). The PostID is an identity column. Title and Description are nvarchar columns and PublishDate is a datetime column.

The ASP.NET MVC application that you wish to create is supposed to display blog posts residing in this table. Under default configuration you would have requested a blog post with a URL like this:

http://localhost/Blog/Show/10

In the above URL, Blog is the name of the controller, Show is the name of an action method and 10 is the PostID of a blog post to display. The Show() action method is shown below:

public class BlogController : Controller
{
  public ActionResult Show(int id)
  {
    BlogDbEntities db = new BlogDbEntities();
    BlogPost post = db.BlogPosts.Find(id);
    return View(post);
  }
}

As you can see, Blog controller contains Show() action method that takes the id parameter. Inside, the Show() action method retrieves a BlogPost entity for the specified id and passes it to the Show view as its model. The Show view simply displays the post in the browser:

@model AttributeRoutingDemo.Models.BlogPost

<!DOCTYPE html>
<html>
<head>
    <title>ShowPost</title>
</head>
<body>
    <h1>@Model.Title</h1>
    <p>@Model.Content</p>
    <hr />
    <div>Published on : @Model.PublishDate</div>
</body>
</html>

Where is the default route in this example? If you open RouteConfig.cs file located in the App_Start folder you will find this route definitation:

routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home",
                    action = "Index",
                    id = UrlParameter.Optional }
);

Here, the Default route definition uses {controller} and {action} parameters to map the URL to a controller and its action respectively. The default values for controller and action are specified to be Home and Index respectively.

Defining a Simple Route

In the previous section you developed a simple ASP.NET MVC application that displays a blog post. The default route used in the application was defined in RouteConfig.cs level. Now, let’s use attribute routing to achieve the same effect. Open RouteConfig.cs and add the following call to the RegisterRoutes() method:

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

The above code calls the MapMvcAttributeRoutes() method of the RouteCollection so that attribute routing is enabled for the application. Next, open the BlogController.cs and add the following code:

[Route("{action=Show}/{id?}")]
public class BlogController : Controller
{
  ...
}

The above code uses the [Route] attribute on top of the BlogController class to define the default route. The [Route] attribute specifies the template of the route. The template indicates that the action route parameter has a default value of Show. The id parameter is optional as indicated by the ? character.

After making these changes if you run the application it should run as expected even if routes.MapRoute() call has been removed from the RouteConfig.cs file.

Defining a Custom Route

Now, let’s assume that you wish to generate URLs of the following form:

http://localhost/2013/12/20/1000

In the above URL, year, month, day (of the publication of a post) and PostID of a blog  post is embedded with the URL itself. This URL is to be mapped with the Blog controller and Show() action method. To accomplish this task add the [Route] attribute to the Show() action method as shown below:

[Route("{year}/{month}/{day}/{postid}")]
public ActionResult Show(int year,int month,int day,int postid)
{
    BlogDbEntities db = new BlogDbEntities();
    BlogPost post = db.BlogPosts.Find(postid);
    string view = "";
    if(post==null)
    {
        ViewBag.Message = "Invalid Blog Post ID!";
        view = "Error";
    }
    else
    {
        DateTime dt = new DateTime(year, month, day);
        if(dt!=post.PublishDate.Value)
        {
            ViewBag.Message = "Invalid Blog Post Date!";
            view = "Error";
        }
        else
        {
            view = "Show";
        }
    }
    return View(view, post);
}

This time the [Route] attribute is placed on top of the Show() action method and defines four route parameters – year, month, day and postid. The Show() action method now takes four parameters corresponding to the route parameter. This way the values of year, month, day and postid can be received in the Show() method.

Inside, the Show() method retrieves the BlogPost entity for the specified PostID and also constructs a DateTime instance based on the values of year, month and day. If there is no blog post  associated with the postid supplied in the URL, an error message is stored in the ViewBag. On the same lines, if the publication date specified in the URL doesn’t match with the actual publication date an error message is stored in the ViewBag. If there is any error the Error view is displayed in the browser, otherwise the Show view is displayed.

The following figure shows a sample run of the application with the newly defined route:

A sample run of the application
A sample run of the application

Notice how the URL reflects the newly defined route. If you try to enter some non existent PostID (or PublishDate) you will get an error as shown:

Invalid blog post ID
Invalid blog post ID

Route Prefixes

In the previous example, the route had four route parameters – values that are changing based on the blog post to access. You can also specify a static route prefix for a route. For example, you may want to have URLs in the following form:

http://localhost/myblog/2013/12/10/1000

In the above URL, myblog is a route prefix and remains static no matter which blog post is being displayed. You can add a route prefix to your routes in two ways – defining them in the route template OR defining them using the [RoutePrefix] attribute.

Let’s see the first technique mentioned above. Modify the route template as shown below:

[Route("myblog/{year}/{month}/{day}/{postid}")]
public ActionResult Show(int year,int month,int day,int postid)
{
  ...
}

As you can see, the route template now includes the route prefix – myblog – followed by four route parameters as before. Henceforth, your URLs will be of the format shown below:

The route prefix - myblog
The route prefix – myblog

To set a route prefix for all the action methods you can use the [RoutePrefix] attribute on top of the controller as shown below:

[RoutePrefix("myblog")]
public class BlogController : Controller
{
  ...
}

Once you decorate the controller with the [RoutePrefix] attribute all the routes defined on the action methods automatically assume this route prefix. Thus the Show() action method needs do define a route like this:

[Route("{year}/{month}/{day}/{postid}")]
public ActionResult Show(int year,int month,int day,int postid)
{
  ...
}

Since the route prefix is defined using the [RoutePrefix] attribute the route template doesn’t include it.

What if you wish to deviate from the route prefix defined using the [RoutePrefix] attribute? You can do so by ~ qualifying the route template. Here is how you can do that:

[Route("~/myposts/{year}/{month}/{day}/{postid}")]
public ActionResult Show(int year,int month,int day,int postid)
{
  ...
}

The above code shows the [Route] attribute defining a route template that begins with ~/myposts. Doing so will bypass the route prefix defined by the [RoutePrefix] attribute and the new route prefix (myposts in this case) will be used. 

Route Constraints

The URLs can always be tampered by the end user. What if a user enters string values in place of year, month and day? What if a user specifies some invalid value, say 100, for month? Obviously somewhere in the code you will get an exception. However, the URL is successfully “accepted” by your code and then an exception is thrown. Wouldn’t it be nice to detect such violations before control reaches your code? Luckily, route constraints allow you to do just that. Using route constraints you can place some criteria for the route parameter values. Let’s see how route constraints can be placed for our example:

 [Route("{year:int:minlength(4):maxlength(4)}/{month:int:min(1):max(12)}/{day:int:range(1,31)}/{postid}")]
public ActionResult ShowPostConstraint(int year, int month, int day, int postid)
{
  ...
}

The above code shows a route that makes use of route constraints. Route constraints are added to a route parameter by appending them using :. Thus the above code uses the following route constraints:

  • :int
  • :minlength(n)
  • :maxlength(n)
  • :min(n)
  • :max(n)
  • :range(n,m)

The :int route constraint indicates that the parameter under consideration must contain an integer value. The min() and max() route constraints indicate that value of a parameter must be minimum and maximum up to the specified limit. The minlength() and maxlength() route constraints are used to ensure that the string length of the parameter is minimum or maximum up to the specified value. Finally, the range() route constraint ensures that a value is within minimum and maximum range. There are many other route constraints not covered in the above example and you can have a quick look at them here.

If you try to enter some invalid value (say, characters instead of number for year parameter) in the URL you will get an error even before reaching your code.

Invaid value error
Invaid value error

As you can see the web server is throwing 404 – Not Found error indicating that the control didn’t reach the Show() action method.

Summary

ASP.NET MVC 5 introduced attribute routing that simplifies the routing mechanism in MVC applications. The [Route] attribute provided by the attribute routing can be used to define a route template. If used on action methods the [Route] attribute defines a route that lands a matching request to the action method under consideration. If used on controller it can use the {action} parameter to map a request with an action. You can also define route prefixes using the [RoutePrefix] attribute or embedding the route prefix in the route template. Finally, you can also apply constraints to the route parameters so that they contain values meeting certain criteria.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read