Dot Net Stuff

Understanding URL Rewriting and URL Attribute Routing in ASP.NET MVC (MVC5) with Examples


Url Rewriting and Routing is how ASP.NET MVC matches a URI to an action. MVC 5 supports a new type of routing, called attribute routing, which is also works in ASP.NET vNext. Attribute routing uses attributes to define routes. Attribute routing provides us more control over the URIs in your web application.

The MVC4 or earlier style of routing, which is called convention-based routing, is still fully supported. In fact, the major advantage of MVC5 is that, we can combine both techniques in the same application. In this article I will try to cover the basic features and options of Attribute Routing and Url Rewriting in ASP.NET MVC5.

How to Enabling Attribute Routing in MVC5

To enable attribute routing, call MapMvcAttributeRoutes during configuration. Following are the code snipped.

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

In MVC5, we can combine attribute routing with convention-based routing. Following are the code snipped.

 
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); 
    routes.MapMvcAttributeRoutes(); 
    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

Optional URI Parameters and Default Values in Attribute Routing in MVC5

It is very easy to make a URI parameter optional by adding a question mark to the route parameter. We can also specify a default value by using the form parameter=value. Following are a code sample shows, how it can be done.

 
public class StudentController : Controller
{
    // eg: /students
    // eg: /students/1
    [Route("students/{id?}")]
    public ActionResult View(string id)
    {
        if (!String.IsNullOrEmpty(id))
        {
            return View("ViewStudent", GetStudent(id));
        }
        return View("AllStudents", GetStudents());
    }
 
    // eg: /students/category
    // eg: /students/category/general
    // eg: /students/category/obc
    [Route("students/category/{category=general}")]
    public ActionResult ViewByCategory(string category)
    {
        return View("ViewStudent", GetStudentByCategory(category));
    }
}

In this example, both /students and /students/1 will route to the “View” action, the former will result with listing all students, and the latter will list the specific student. Both /students/category and /students/category/general will be treated the same.

Route Prefixes (Prefix in Url in MVC)

N ASP.NET MVC5 the routes in a controller all start with the same prefix. For example:

 
public class StudentsController : Controller
{
    // eg: /students
    [Route("students")]
    public ActionResult Index() { ... }
    // eg: /students/5
    [Route("students/{studentId}")]
    public ActionResult Show(int studentId) { ... }
    // eg: /students/5/edit
    [Route("students/{studentId}/edit")]
    public ActionResult Edit(int studentId) { ... }
}

To achieve same features, we can set a common prefix for an entire controller by using the [RoutePrefix] attribute. Following are the code sample.

 
[RoutePrefix("students ")]
public class StudentsController : Controller
{
    // eg.: /students
    [Route]
    public ActionResult Index() { ... }
    // eg.: /students/5
    [Route("{studentId}")]
    public ActionResult Show(int studentId) { ... }
    // eg.: /students/5/edit
    [Route("{studentId}/edit")]
    public ActionResult Edit(int studentId) { ... }
}

To change this Url Prefix, we must use a tilde (~) on the method attribute to override the route prefix. Following code sample shows how it can be done.

 
[RoutePrefix("students")]
public class StudentsController : Controller
{
    // eg.: /college-students
    [Route("~/college-students")]
    public ActionResult ShowStudents() { ... }
    //TODO: any logic
    ...
}

Default Route in MVC5

In some context we may want to apply the [Route] attribute on the controller level, and trying to capture the action as a parameter. That route would then be applied on all actions in the controller, unless a specific [Route] has been defined on a specific action, overriding the default set on the controller. Following are a code sample to do same.

 
[RoutePrefix("students")]
[Route("{action=index}")]
public class StudentsController : Controller
{
    // eg.: /students
    public ActionResult Index() { ... }
 
    // eg.: /students/archive
    public ActionResult Archive() { ... }
 
    // eg.: /students/new
    public ActionResult New() { ... }
 
    // eg.: /students/edit/5
    [Route("edit/{studentId:int}")]
    public ActionResult Edit(int studentId) { ... }
}
Route Constraints in MVC5
Route constraints allow us to restrict how the parameters in the route template are matched. The general syntax is {parameter:constraint}. Following code sample shows, how it could be achieved:
// eg: /students/5
[Route("students/{id:int}"]
public ActionResult GetStudentById(int id) { ... }
 
// eg: students/ramesh
[Route("students/{name}"]
public ActionResult GetStudentByName(string name) { ... }

In above code sample, the first route will only be selected if the "id" segment of the URI is an integer. Otherwise, the second route will be chosen. It is one of the best features on new Attribute Routing. Following table lists the constraints that are supported.

ConstraintDescriptionExample
alphaIt Matches uppercase or lowercase Latin alphabet characters (a-z, A-Z){x:alpha}
boolIt Matches a Boolean value.{x:bool}
datetimeIt Matches a DateTime value.{x:datetime}
decimalIt Matches a decimal value.{x:decimal}
doubleIt Matches a 64-bit floating-point value.{x:double}
floatIt Matches a 32-bit floating-point value{x:float}
guidIt Matches a GUID value.{x:guid}
intIt Matches a 32-bit integer value.{x:int}
lengthIt Matches a string with the specified length or within a specified range
of lengths.
{x:length(6)}
{x:length(1,20)}
longIt Matches a 64-bit integer value.{x:long}
maxIt Matches an integer with a maximum value.{x:max(10)}
maxlengthIt Matches a string with a maximum length.{x:maxlength(10)}
minIt Matches an integer with a minimum value.{x:min(10)}
minlengthIt Matches a string with a minimum length.{x:minlength(10)}
rangeIt Matches an integer within a range of values.{x:range(10,50)}
regexIt Matches a regular expression.{x:regex(^\d{3}-\d{3}-\d{4}$)}

Notice that some of the constraints, such as "min", take arguments in parentheses. We can apply multiple constraints to a parameter, separated by a colon. To Specifying that a parameter is Optional (via the '?' modifier) should be done after inline constraints. Following are the code examples:

 
[Route("students/{id:int:min(1)?}")]
public ActionResult GetStudentById(int id) { ... }

Custom Route Constraints for URL Rewriting

If we want to create custom route constraints, this can be achieved by implementing the IRouteConstraint interface. For example, the following constraint restricts a parameter to set of valid values:

 
public class ValuesConstraint : IRouteConstraint
{
    private readonly string[] validOptions;
    public ValuesConstraint(string options)
    {
        validOptions = options.Split('|');
    }
 
    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        object value;
        if (values.TryGetValue(parameterName, out value) && value != null)
        {
            return validOptions.Contains(value.ToString(), StringComparer.OrdinalIgnoreCase);
        }
        return false;
    }
}

In above code we have defined our custom Route Constraints. We need to register this Route Constraint, Following code sample shows, how to register custom Route Constraints.

 
public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
        var constraintsResolver = new DefaultInlineConstraintResolver();
 
        constraintsResolver.ConstraintMap.Add("values", typeof(ValuesConstraint));
 
        routes.MapMvcAttributeRoutes(constraintsResolver);
    }
}

Now, we can apply this Route Constraints in our MVC application. Given code sample shows the same:

 
public class StudentsController : Controller
{
    // eg: show/active and /show/inactive but 
    [Route("show/{status:values(active|inactive)}")]
    public ActionResult Show(string status)
    {
        return Content("Student is " + status);
    }
}

Route Names

We can specify a name for a route, this name will help us to easily allow URI generation for it in our application. For example, for the following route:

 
[Route("dashboard", Name = "admindashboard")]
public ActionResult AdminDashboard() { ... }

We could generate a link using Url.RouteUrl:

 
<a href="@Url.RouteUrl(" admindashboard")">Main Dashboard</a>

Areas

Areas in MVC is one of the important features to be used in categories code files in large application. We can define that a controller belongs to an area by using the [RouteArea] attribute. When doing so, you can safely remove the AreaRegistration class for that area.

 
[RouteArea("Admin")]
[RoutePrefix("dashboard")]
[Route("{action}")]
public class AdminController : Controller
{
    // eg: /admin/dashboard/login
    public ActionResult Login() { ... }
 
    // eg: /admin/dashboard/show-options
    [Route("show-options")]
    public ActionResult Options() { ... }
 
    // eg: /stats
    [Route("~/stats")]
    public ActionResult Stats() { ... }
}

With the help of above controller, the following link generation call will result with the string "/Admin/dashboard/show-options"

 
Url.Action("Options", "Dashboard", new { Area = "Admin" })

MVC5 also allow us to set up our custom prefix for the area that defer from the area name, We need to use AreaPrefix named parameter, for example:

 
[RouteArea("BackOffice", AreaPrefix = "back-office")]

In some cases, there may be requirement to use both Areas and Route Attributes and may be Areas with convention based routes, Which is set by an AreaRegistration class, then you need to make sure that area registration happen after MVC attribute routes are configured, however before the default convention-based route is set. The reason behind this is that route registration should be ordered from the most specific (attributes) through more general (area registration) to the mist generic (the default route) to avoid generic routes from “hiding” more specific routes by matching incoming requests too early in the pipeline. Following are code snipped to do the same:

 
public static void RegisterRoutes(RouteCollection routes)
{
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
    routes.MapMvcAttributeRoutes();
 
    AreaRegistration.RegisterAllAreas();
 
    routes.MapRoute(
        name: "Default",
        url: "{controller}/{action}/{id}",
        defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
    );
}

Summary: in this article I have discussed the Route Attributes in MVC5 and how we can create different urls as par different scenarios. We have also learn to create custom Route Constraints also to uses Areas in our MVC application. I am sure, that you will find this article very useful.


Keen to hear from you...!

If you have any questions related to what's mentioned in the article or need help with any issue, ask it, I would love to here from you. Please MakeUseOf Contact and i will be more than happy to help.

About the author

Anil Sharma is Chief Editor of dotnet-stuff.com. He's a software professional and loves to work with Microsoft .Net. He's usually writes articles about .Net related technologies and here to shares his experiences, personal notes, Tutorials, Examples, Problems & Solutions, Code Snippets, Reference Manual and Resources with C#, Asp.Net, Linq , Ajax, MVC, Entity Framework, WCF, SQL Server, jQuery, Visual Studio and much more...!!!

blog comments powered by Disqus