Click here to Skip to main content
13,190,404 members (51,266 online)
Click here to Skip to main content
Add your own
alternative version

Stats

115.3K views
6.8K downloads
89 bookmarked
Posted 10 Jun 2015

RESTful Day #4: Custom URL Re-Writing/Routes using Attribute Routing in MVC 4 Web APIs

, 1 Mar 2016
Rate this:
Please Sign up or sign in to vote.
In this article I’ll explain how to write your own custom routes using Attribute Routing.

Table of Contents

Introduction

We have already learnt a lot on WebAPI. I have already explained how to create WebAPI, connect it with database using Entity Framework, resolving dependencies using Unity Container as well as using MEF. In all our sample applications we were using default route that MVC provides us for CRUD operations. In this article I’ll explain how to write your own custom routes using Attribute Routing. We’ll deal with Action level routing as well as Controller level routing. I’ll explain this in detail with the help of a sample application. My new readers can use any Web API sample they have, else you can also use the sample applications we developed in my previous articles.

Roadmap

Let’s revisit the road map that I started on Web API,

Here is my roadmap for learning RESTful APIs,

I’ll purposely use Visual Studio 2010 and .NET Framework 4.0 because there are few implementations that are very hard to find in .NET Framework 4.0, but I’ll make it easy by showing how we can do it.

Routing

Routing in generic terms for any service, API, website is a kind of pattern defining system that tries to maps all request from the clients and resolves that request by providing some response to that request. In WebAPI we can define routes in WebAPIConfig file, these routes are defined in an internal Route Table. We can define multiple sets of Routes in that table.

Existing Design and Problem

We already have an existing design. If you open the solution, you’ll get to see the structure as mentioned below,

In our existing application, we created WebAPI with default routes as mentioned in the file named WebApiConfig in App_Start folder of WebApi project. The routes were mentioned in the Register method as,

config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

Do not get confused by MVC routes, since we are using MVC project we also get MVC routes defined in RouteConfig.cs file as,

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

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

We need to focus on the first one i.e. WebAPI route. As you can see in the following image, what each property signifies,

We have a route name, we have a common URL pattern for all routes, and option to provide optional parameters as well.

Since our application do not have particular different action names, and we were using HTTP VERBS as action names, we didn’t bother much about routes. Our Action names were like,

1.	public HttpResponseMessage Get()
2.	public HttpResponseMessage Get(int id)
3.	public int Post([FromBody] ProductEntity productEntity)
4.	public bool Put(int id, [FromBody]ProductEntity productEntity)
5.	public bool Delete(int id)

Default route defined does not takes HTTP VERBS action names into consideration and treat them default actions, therefore it does not mentions {action} in routeTemplate. But that’s not limitation, we can have our own routes defined in WebApiConfig, for example, check out the following routes,

public static void Register(HttpConfiguration config)
      {
          config.Routes.MapHttpRoute(
              name: "DefaultApi",
              routeTemplate: "api/{controller}/{id}",
              defaults: new { id = RouteParameter.Optional }
          );

          config.Routes.MapHttpRoute(
             name: "ActionBased",
             routeTemplate: "api/{controller}/{action}/{id}",
             defaults: new { id = RouteParameter.Optional }
         );
          config.Routes.MapHttpRoute(
            name: "ActionBased",
            routeTemplate: "api/{controller}/action/{action}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
      }

And etc. In above mentioned routes, we can have action names as well, if we have custom actions.

So there is no limit to define routes in WebAPI. But there are few limitations to this, note we are talking about WebAPI 1 that we use with .NET Framework 4.0 in Visual Studio 2010. Web API 2 has overcome those limitations by including the solution that I’ll explain in this article. Let’s check out the limitations of these routes,

Yes, these are the limitations that I am talking about in Web API 1.

If we have route template like routeTemplate: "api/{controller}/{id}" or routeTemplate: "api/{controller}/{action}/{id}" or routeTemplate: "api/{controller}/action/{action}/{id}",

We can never have custom routes and will have to slick to old route convention provided by MVC. Suppose your client of the project wants to expose multiple endpoints for the service, he can’t do so. We also can not have our own defined names for the routes, so lots of limitation.

Let’s suppose we want to have following kind of routes for my web API endpoints, where I can define versions too,

v1/Products/Product/allproducts
v1/Products/Product/productid/1
v1/Products/Product/particularproduct/4
v1/Products/Product/myproduct/<with a range>
v1/Products/Product/create
v1/Products/Product/update/3

and so on, then we cannot achieve this with existing model.

Fortunately these things have been taken care of in WebAPI 2 with MVC 5, but for this situation we have AttributeRouting to resolve and overcome these limitations.

Attribute Routing

Attribute Routing is all about creating custom routes at controller level, action level. We can have multiple routes using Attribute Routing. We can have versions of routes as well, in short we have the solution for our exiting problems. Let’s straight away jump on how we can implement this in our exising project. I am not explaining on how to create a WebAPI, for that you can refer my first post of the series.

Step 1: Open the solution ,and open Package Manage Console like shown below in the figure,

Goto Tools->Library Packet manage-> Packet Manager Console

Step 2: In the package manager console window at left corner of Visual Studio type, Install-Package AttributeRouting.WebApi, and choose the project WebApi or your own API project if you are using any other code sample, then press enter.

Step 3: As soon as the package is installed, you’ll get a class named AttributeRoutingHttpConfig.cs in your App_Start folder.

This class has its own method to RegisterRoutes, which internally maps attribute routes. It has a start method that picks Routes defined from GlobalConfiguration and calls the RegisterRoutes method,

using System.Web.Http;
using AttributeRouting.Web.Http.WebHost;

[assembly: WebActivator.PreApplicationStartMethod(typeof(WebApi.AttributeRoutingHttpConfig), "Start")]

namespace WebApi 
{
    public static class AttributeRoutingHttpConfig
	{
		public static void RegisterRoutes(HttpRouteCollection routes) 
		{    
			// See http://github.com/mccalltd/AttributeRouting/wiki for more options.
			// To debug routes locally using the built in ASP.NET development server, go to /routes.axd

            routes.MapHttpAttributeRoutes();
		}

        public static void Start() 
		{
            RegisterRoutes(GlobalConfiguration.Configuration.Routes);
        }
    }
}

We don’t even have to touch this class, our custom routes will automatically be taken care of using this class.We just need to focus on defining routes. No coding :) You can now use route specific stuff like route names, verbs, constraints, optional parameters, default parameters, methods, route areas, area mappings, route prefixes, route conventions etc.

Setup REST endpoint / WebAPI project to define Routes

Our 90% of the job is done.

We now need to setup or WebAPI project and define our routes.

Our existing ProductController class looks something like shown below,

using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using BusinessEntities;
using BusinessServices;

namespace WebApi.Controllers
{
    public class ProductController : ApiController
    {

        private readonly IProductServices _productServices;

        #region Public Constructor

        /// <summary>
        /// Public constructor to initialize product service instance
        /// </summary>
        public ProductController(IProductServices productServices)
        {
            _productServices = productServices;
        }

        #endregion

        // GET api/product
        public HttpResponseMessage Get()
        {
            var products = _productServices.GetAllProducts();
            var productEntities = products as List<ProductEntity> ?? products.ToList();
            if (productEntities.Any())
                return Request.CreateResponse(HttpStatusCode.OK, productEntities);
            return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Products not found");
        }

        // GET api/product/5
        public HttpResponseMessage Get(int id)
        {
            var product = _productServices.GetProductById(id);
            if (product != null)
                return Request.CreateResponse(HttpStatusCode.OK, product);
            return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No product found for this id");
        }

        // POST api/product
        public int Post([FromBody] ProductEntity productEntity)
        {
            return _productServices.CreateProduct(productEntity);
        }

        // PUT api/product/5
        public bool Put(int id, [FromBody] ProductEntity productEntity)
        {
            if (id > 0)
            {
                return _productServices.UpdateProduct(id, productEntity);
            }
            return false;
        }

        // DELETE api/product/5
        public bool Delete(int id)
        {
            if (id > 0)
                return _productServices.DeleteProduct(id);
            return false;
        }
    }
}

Where we have a controller name Product and Action names as Verbs. When we run the application, we get following type of endpoints only (please ignore port and localhost setting. It’s because I run this application from my local environment),

Get All Products:

http://localhost:40784/api/Product

Get product By Id:

http://localhost:40784/api/Product/3

Create product :

http://localhost:40784/api/Product (with json body)

Update product:

http://localhost:40784/api/Product/3 (with json body)

Delete product:

http://localhost:40784/api/Product/3

Step 1: Add two namespaces to your controller,

using AttributeRouting;
using AttributeRouting.Web.Http;

Step 2: Decorate your action with different routes,

Like in above image, I defined a route with name productid which taked id as a parameter. We also have to provide verb (GET, POST, PUT, DELETE, PATCH) along with the route like shown in image. So it is [GET("productid/{id?}")]. You can define whatever route you want for your Action like [GET("product/id/{id?}")], [GET("myproduct/id/{id?}")] and many more.

Now when I run the application and navigate to /help page, I get this,

i.e. I got one more route for Getting product by id. When you’ll test this service you’ll get your desired URL something like: http://localhost:55959/Product/productid/3, that sounds like real REST :)

Similarly decorate your Action with multiple routes like show below,

// GET api/product/5
[GET("productid/{id?}")]
[GET("particularproduct/{id?}")]
[GET("myproduct/{id:range(1, 3)}")]
public HttpResponseMessage Get(int id)
{
    var product = _productServices.GetProductById(id);
    if (product != null)
        return Request.CreateResponse(HttpStatusCode.OK, product);
    return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No product found for this id");
}

Therefore we see, we can have our custom route names and as well as multiple endpoints for a single Action. That’s exciting.Each endpoint will be different but will serve same set of result.

  • {id?} : here ‘?’ means that parameter can be optional.
  • [GET("myproduct/{id:range(1, 3)}")], signifies that the product id’s falling in this range will only be shown.

More Routing Constraints

You can leverage numerous Routing Constraints provided by Attribute Routing. I am taking example of some of them,

Range

To get the product within range, we can define the value, on condition that it should exist in database.

[GET("myproduct/{id:range(1, 3)}")]
public HttpResponseMessage Get(int id)
{
    var product = _productServices.GetProductById(id);
    if (product != null)
        return Request.CreateResponse(HttpStatusCode.OK, product);
    return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No product found for this id");
}

Regular Expression

You can use it well for text/string parameters more efficiently.

        [GET(@"id/{e:regex(^[0-9]$)}")]
        public HttpResponseMessage Get(int id)
        {
            var product = _productServices.GetProductById(id);
            if (product != null)
                return Request.CreateResponse(HttpStatusCode.OK, product);
            return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No product found for this id");
        }

e.g. [GET(@"text/{e:regex(^[A-Z][a-z][0-9]$)}")]

Optional Parameters and Default Parameters

You can also mark the service parameters as optional in the route. For example you want to fetch an employee detail from the data base with his name,

[GET("employee/name/{firstname}/{lastname?}")]
public string GetEmployeeName(string firstname, string lastname="mittal")
{
   …………….
  ……………….
}

In the above mentioned code, I marked last name as optional by using question mark ‘?’ to fetch the employee detail. It’s my end user wish to provide the last name or not.

So the above endpoint could be accessed through GET verb with urls,

~/employee/name/akhil/mittal
~/employee/name/akhil

If a route parameter defined is marked optional, you must also provide a default value for that method parameter.

In the above example, I marked ‘lastname’ as an optional one and so provided a default value in the method parameter , if user doesn’t send any value, "mittal" will be used.

In .NET 4.5 Visual Studio > 2010 with WebAPI 2, you can define DefaultRoute as an attribute too, just try it by your own. Use attribute [DefaultRoute] to define default route values.

You can try giving custom routes to all your controller actions.

I marked my actions as,

// GET api/product
[GET("allproducts")]
[GET("all")]
public HttpResponseMessage Get()
{
    var products = _productServices.GetAllProducts();
    var productEntities = products as List<ProductEntity> ?? products.ToList();
    if (productEntities.Any())
        return Request.CreateResponse(HttpStatusCode.OK, productEntities);
    return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Products not found");
}

// GET api/product/5
[GET("productid/{id?}")]
[GET("particularproduct/{id?}")]
[GET("myproduct/{id:range(1, 3)}")]
public HttpResponseMessage Get(int id)
{
    var product = _productServices.GetProductById(id);
    if (product != null)
        return Request.CreateResponse(HttpStatusCode.OK, product);
    return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No product found for this id");
}

// POST api/product
[POST("Create")]
[POST("Register")]
public int Post([FromBody] ProductEntity productEntity)
{
    return _productServices.CreateProduct(productEntity);
}

// PUT api/product/5
[PUT("Update/productid/{id}")]
[PUT("Modify/productid/{id}")]
public bool Put(int id, [FromBody] ProductEntity productEntity)
{
    if (id > 0)
    {
        return _productServices.UpdateProduct(id, productEntity);
    }
    return false;
}

// DELETE api/product/5
[DELETE("remove/productid/{id}")]
[DELETE("clear/productid/{id}")]
[PUT("delete/productid/{id}")]
public bool Delete(int id)
{
    if (id > 0)
        return _productServices.DeleteProduct(id);
    return false;
}

And therefore get the routes as,

 

GET

POST / PUT / DELETE

Check for more constraints here.

You must be seeing "v1/Products" in every route, that is due to RoutePrefix I have used at controller level. Let’s discuss RoutePrefix in detail.

RoutePrefix: Routing at Controller level

We were marking our actions with particular route, but guess what, we can mark our controllers too with certain route names, we can achieve this by using RoutePrefix attribute of AttributeRouting. Our controller was named Product, and I want to append Products/Product before my every action, there fore without duplicating the code at each and every action, I can decorate my Controller class with this name like shown below,

[RoutePrefix("Products/Product")]
public class ProductController : ApiController
{

Now, since our controller is marked with this route, it will append the same to every action too. For e.g. route of following action,

[GET("allproducts")]
[GET("all")]
public HttpResponseMessage Get()
{
    var products = _productServices.GetAllProducts();
    var productEntities = products as List<ProductEntity> ?? products.ToList();
    if (productEntities.Any())
        return Request.CreateResponse(HttpStatusCode.OK, productEntities);
    return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Products not found");
}

Now becomes,

~/Products/Product/allproducts
~/Products/Product/all

RoutePrefix: Versioning

Route prefix can also be used for versioning of the endpoints, like in my code I provided "v1" as version in my RoutePrefix as shown below,

[RoutePrefix("v1/Products/Product")]
public class ProductController : ApiController
{

Therefore "v1" will be appended to every route / endpoint of the service. When we release next version, we can certainly maintain a change log separately and mark the endpoint as "v2" at controller level, that will append "v2" to all actions,

e.g.

~/v1/Products/Product/allproducts
~/v1/Products/Product/all

~/v2/Products/Product/allproducts
~/v2/Products/Product/all

RoutePrefix: Overriding

This functionality is present in .NET 4.5 with Visual Studio > 2010 with WebAPI 2. You can test it there.

There could be situations that we do not want to use RoutePrefix for each and every action. AttributeRouting provides such flexibility too, that despite of RoutePrefix present at controller level, an individual action could have its own route too. It just need to override the default route like shown below,

RoutePrefix at Controller

 [RoutePrefix("v1/Products/Product")]
public class ProductController : ApiController
{

Independent Route of Action

[Route("~/MyRoute/allproducts")]
 public HttpResponseMessage Get()
 {
     var products = _productServices.GetAllProducts();
     var productEntities = products as List<ProductEntity> ?? products.ToList();
     if (productEntities.Any())
         return Request.CreateResponse(HttpStatusCode.OK, productEntities);
     return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Products not found");
 }

Disable Default Routes

You must be wondering that in the list of all the URL’s on service help page, we are getting some different/other routes that we have not defined through attribute routing starting like ~/api/Product. These routes are the outcome of default route provided in WebApiConfig file, remember? If you want to get rid of those unwanted routes, just go and comment everything written in Register method in WebApiConfig.cs file under Appi_Start folder,

//config.Routes.MapHttpRoute(
//    name: "DefaultApi",
//    routeTemplate: "api/{controller}/{id}",
//    defaults: new { id = RouteParameter.Optional }
//);

You can also remove the complete Register method, but for that you need to remove its calling too from Global.asax file.

Running the application

Just run the application, we get,

We already have our test client added, but for new readers, just go to Manage Nuget Packages, by right clicking WebAPI project and type WebAPITestClient in searchbox in online packages,

You’ll get "A simple Test Client for ASP.NET Web API", just add it. You’ll get a help controller in Areas-> HelpPage like shown below,

I have already provided the database scripts and data in my previous article, you can use the same.

Append "/help" in the application url, and you’ll get the test client,

GET

POST

PUT

DELETE

You can test each service by clicking on it. Once you click on the service link, you'll be redirected to test the service page of that particular service. On that page there is a button Test API in the right bottom corner, just press that button to test your service,

Service for Get All products,

Likewise you can test all the service endpoints.

Conclusion

We now know how to define our custom endpoints and what its benefits are. Just to share that this library was introduced by Tim Call, author of http://attributerouting.net and Microsoft has included this in WebAPI 2 by default. My next article will explain token based authentication using ActionFilters in WepAPI. 'til then Happy Coding :) you can also download the complete source code from GitHub. Add the required packages, if they are missing in the source code.

Click Github Repository to browse for complete source code.

References

http://blogs.msdn.com/b/webdev/archive/2013/10/17/attribute-routing-in-asp-net-mvc-5.aspx

https://github.com/mccalltd/AttributeRouting

My other series of articles

MVC: http://www.codeproject.com/Articles/620195/Learning-MVC-Part-Introduction-to-MVC-Architectu

OOP: http://www.codeproject.com/Articles/771455/Diving-in-OOP-Day-Polymorphism-and-Inheritance-Ear

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Akhil Mittal
Architect Magic Software Inc.
India India
This member doesn't quite have enough reputation to be able to display their biography and homepage.
Group type: Collaborative Group

575 members


You may also be interested in...

Pro
Pro

Comments and Discussions

 
SuggestionOAuth implementation Pin
samalert38-Jun-16 23:00
membersamalert38-Jun-16 23:00 
Questionaction route Pin
ecrap26-Apr-16 4:54
memberecrap26-Apr-16 4:54 
AnswerRe: action route Pin
Akhil Mittal 1-May-16 20:38
mvp Akhil Mittal 1-May-16 20:38 
QuestionVery Helpfull Pin
Ashish Bachhav10-Jan-16 23:42
memberAshish Bachhav10-Jan-16 23:42 
AnswerRe: Very Helpfull Pin
Akhil Mittal 12-Jan-16 17:25
mvp Akhil Mittal 12-Jan-16 17:25 
PraiseExcellent Article Pin
treewqa29-Oct-15 16:54
membertreewqa29-Oct-15 16:54 
GeneralRe: Excellent Article Pin
Akhil Mittal 1-Nov-15 20:06
mvp Akhil Mittal 1-Nov-15 20:06 
GeneralMy vote of 5 Pin
D V L1-Oct-15 2:43
professionalD V L1-Oct-15 2:43 
GeneralRe: My vote of 5 Pin
Akhil Mittal 2-Oct-15 5:29
mvp Akhil Mittal 2-Oct-15 5:29 
GeneralMy vote of 5 Pin
Member 101973836-Jul-15 4:02
memberMember 101973836-Jul-15 4:02 
GeneralRe: My vote of 5 Pin
Akhil Mittal 6-Jul-15 4:07
mvp Akhil Mittal 6-Jul-15 4:07 
GeneralMy vote of 5 Pin
rediffAM15-Jun-15 7:29
memberrediffAM15-Jun-15 7:29 
GeneralRe: My vote of 5 Pin
Akhil Mittal 15-Jun-15 18:49
mvp Akhil Mittal 15-Jun-15 18:49 
QuestionOne Query Pin
codewaver15-Jun-15 7:14
membercodewaver15-Jun-15 7:14 
AnswerRe: One Query Pin
Akhil Mittal 15-Jun-15 18:49
mvp Akhil Mittal 15-Jun-15 18:49 
GeneralMy vote of 5 Pin
codewaver15-Jun-15 7:08
membercodewaver15-Jun-15 7:08 
GeneralRe: My vote of 5 Pin
Akhil Mittal 15-Jun-15 18:37
mvp Akhil Mittal 15-Jun-15 18:37 
GeneralMy vote of 5 Pin
mini.mi14-Jun-15 21:23
membermini.mi14-Jun-15 21:23 
GeneralRe: My vote of 5 Pin
Akhil Mittal 14-Jun-15 22:46
mvp Akhil Mittal 14-Jun-15 22:46 
GeneralNice work Pin
Nadeem Jamali12-Jun-15 3:20
memberNadeem Jamali12-Jun-15 3:20 
GeneralRe: Nice work Pin
Akhil Mittal 12-Jun-15 3:34
mvp Akhil Mittal 12-Jun-15 3:34 
QuestionOn Security? Pin
prashita gupta11-Jun-15 7:06
memberprashita gupta11-Jun-15 7:06 
AnswerRe: On Security? Pin
Akhil Mittal 11-Jun-15 18:48
mvp Akhil Mittal 11-Jun-15 18:48 
AnswerRe: On Security? Pin
Akhil Mittal 1-Jul-15 0:35
mvp Akhil Mittal 1-Jul-15 0:35 
Questionvery good. one question please. Pin
Chris Dis11-Jun-15 5:10
memberChris Dis11-Jun-15 5:10 
AnswerRe: very good. one question please. Pin
Akhil Mittal 11-Jun-15 18:49
mvp Akhil Mittal 11-Jun-15 18:49 
Questiongood Pin
Member 1116838411-Jun-15 1:58
memberMember 1116838411-Jun-15 1:58 
AnswerRe: good Pin
Akhil Mittal 11-Jun-15 2:58
mvp Akhil Mittal 11-Jun-15 2:58 
GeneralMy vote of 5 Pin
Neeraj K. Gupta10-Jun-15 22:56
memberNeeraj K. Gupta10-Jun-15 22:56 
GeneralRe: My vote of 5 Pin
Akhil Mittal 11-Jun-15 1:47
mvp Akhil Mittal 11-Jun-15 1:47 
GeneralMy vote of 5 Pin
atulonweb@gmail.com10-Jun-15 21:46
memberatulonweb@gmail.com10-Jun-15 21:46 
GeneralRe: My vote of 5 Pin
Akhil Mittal 10-Jun-15 22:47
mvp Akhil Mittal 10-Jun-15 22:47 
GeneralMy vote of 5 Pin
sachin verma10-Jun-15 19:19
membersachin verma10-Jun-15 19:19 
GeneralRe: My vote of 5 Pin
Akhil Mittal 10-Jun-15 22:47
mvp Akhil Mittal 10-Jun-15 22:47 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Terms of Use | Mobile
Web02 | 2.8.171016.2 | Last Updated 2 Mar 2016
Article Copyright 2015 by Akhil Mittal
Everything else Copyright © CodeProject, 1999-2017
Layout: fixed | fluid