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

Table of Contents

  • Table of Contents
  • Introduction
  • Roadmap
  • Routing
  • Existing Design and Problem
  • Attribute Routing
  • Setup REST endpoint / WebAPI project to define Routes
  • More Routing Constraints
  1. Range:
  2. Regular Expression: 
  3. Optional Parameters and Default Parameters:
      • RoutePrefix: Routing at Controller level
      • RoutePrefix: Versioning
      • RoutePrefix: Overriding
      • Disable Default Routes
      • Running the application
      • Conclusion
      • References

      Introduction

      We have already learned a lot about WebAPIs. I have already explained how to create a WebAPI application, connect it with a database using the Entity Framework, resolve dependencies using a Unity Container as well as using MEF. In all our sample applications we were using the default route that MVC provides us for CRUD operations. This article explains 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 using 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 began on the Web API.

      Routing

      Here is my roadmap for learning RESTful APIs:

       
       

      I'll intentionally use Visual Studio 2010 and the .NET Framework 4.0 because there are a few implementations that are very hard to find in .NET Framework 4.0, but I'll make it easy by showing how to do it.

      Routing

      Design

      Image credit : routing

      Routing, in generic terms for any service, API or website, is a kind of pattern defining a system that tries to map all the requests from the clients and resolves that request by providing some response to that request. In the WebAPI we can define routes in the 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 specified in the following:

      WebAPI route

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

      1. config.Routes.MapHttpRoute(    
      2.        name: "DefaultApi",    
      3.        routeTemplate: "api/{controller}/{id}",    
      4.        defaults: new { id = RouteParameter.Optional }    
      5. );    
      Do not be confused by MVC routes, since we are using a MVC project we also get MVC routes defined in the RouteConfig.cs file as in the following:
      1. public static void RegisterRoutes(RouteCollection routes)  
      2. {  
      3.     routes.IgnoreRoute("{resource}.axd/{*pathInfo}");  
      4.   
      5.     routes.MapRoute(  
      6.         name: "Default",  
      7.         url: "{controller}/{action}/{id}",  
      8.         defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }  
      9.     );  
      10. }  
      We need to focus on the first one, the WebAPI route. You can see in the following image what each property signifies.

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

      Since our application does not have specific action names and we were using HTTP VERBS as action names, we didn't bother much with 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)  
      The default route defined does not take HTTP Verb action names into consideration and treat them as default actions, therefore it does not specify {action} in routeTemplate. But that's not a limitation, we can have our own routes defined in WebApiConfig. For example, check out the following routes:

      WebApiConfig
      1. public static void Register(HttpConfiguration config)  
      2. {  
      3.     config.Routes.MapHttpRoute(  
      4.         name: "DefaultApi",  
      5.         routeTemplate: "api/{controller}/{id}",  
      6.         defaults: new { id = RouteParameter.Optional }  
      7.     );  
      8.   
      9.     config.Routes.MapHttpRoute(  
      10.        name: "ActionBased",  
      11.        routeTemplate: "api/{controller}/{action}/{id}",  
      12.        defaults: new { id = RouteParameter.Optional }  
      13.    );  
      14.     config.Routes.MapHttpRoute(  
      15.       name: "ActionBased",  
      16.       routeTemplate: "api/{controller}/action/{action}/{id}",  
      17.       defaults: new { id = RouteParameter.Optional }  
      18.   );  
      19. }  
      In the preceding routes, we can have action names as well, if we have custom actions.

      So there is no limit to defining routes in the WebAPI. But there are a few limitations to this. Note that 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.

      limitations of these routes

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

      If we have a route template like routeTemplate: "api/{controller}/{id}" or routeTemplate: "api/{controller}/{action}/{id}" or routeTemplate: "api/{controller}/action/{action}/{id}", then we can never have custom routes and will need to stick to the old route convention provided by MVC. Assume your client of the project wants to expose multiple endpoints for the service, he can't do that. We also cannot have our own defined names for the routes, there are so many limitations.

      Let's assume we want to have the following kinds 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 do this with the 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 the controller level and the 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 to implement this in our existing project. I am not explaining how to create a WebAPI, for that you can refer to my first post of the series.

      Step 1

      Open the solution and open the Package Manage Console as shown in the following figure.

      Go to Tools -> Library Packet manage -> Packet Manager Console.

      Packet Manager Console

      Step 2

      In the package manager console window at the 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.

      WebApi

      Step 3

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

      AttributeRoutingHttpConfig

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

      1. using System.Web.Http;  
      2. using AttributeRouting.Web.Http.WebHost;  
      3.   
      4. [assembly: WebActivator.PreApplicationStartMethod(typeof(WebApi.AttributeRoutingHttpConfig), "Start")]  
      5.   
      6. namespace WebApi   
      7. {  
      8.     public static class AttributeRoutingHttpConfig  
      9.     {  
      10.         public static void RegisterRoutes(HttpRouteCollection routes)   
      11.         {      
      12.             // See http://github.com/mccalltd/AttributeRouting/wiki for more options.  
      13.             // To debug routes locally using the built in ASP.NET development server, go to /routes.axd  
      14.   
      15.             routes.MapHttpAttributeRoutes();  
      16.         }  
      17.   
      18.         public static void Start()   
      19.         {  
      20.             RegisterRoutes(GlobalConfiguration.Configuration.Routes);  
      21.         }  
      22.     }  
      23. }  
      We don't even need 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 and so on.

      Setup REST endpoint / WebAPI project to define Routes

      90% of the job is done.

      done

      We now need to set up our WebAPI project and define our routes.

      Our existing ProductController class looks something as shown in the following:
      1. using System.Collections.Generic;  
      2. using System.Linq;  
      3. using System.Net;  
      4. using System.Net.Http;  
      5. using System.Web.Http;  
      6. using BusinessEntities;  
      7. using BusinessServices;  
      8.   
      9. namespace WebApi.Controllers  
      10. {  
      11.     public class ProductController : ApiController  
      12.     {  
      13.   
      14.         private readonly IProductServices _productServices;  
      15.  
      16.         #region Public Constructor  
      17.   
      18.         /// <summary>  
      19.         /// Public constructor to initialize product service instance  
      20.         /// </summary>  
      21.         public ProductController(IProductServices productServices)  
      22.         {  
      23.             _productServices = productServices;  
      24.         }  
      25.  
      26.         #endregion  
      27.   
      28.         // GET api/product  
      29.         public HttpResponseMessage Get()  
      30.         {  
      31.             var products = _productServices.GetAllProducts();  
      32.             var productEntities = products as List<ProductEntity> ?? products.ToList();  
      33.             if (productEntities.Any())  
      34.                 return Request.CreateResponse(HttpStatusCode.OK, productEntities);  
      35.             return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Products not found");  
      36.         }  
      37.   
      38.         // GET api/product/5  
      39.         public HttpResponseMessage Get(int id)  
      40.         {  
      41.             var product = _productServices.GetProductById(id);  
      42.             if (product != null)  
      43.                 return Request.CreateResponse(HttpStatusCode.OK, product);  
      44.             return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No product found for this id");  
      45.         }  
      46.   
      47.         // POST api/product  
      48.         public int Post([FromBody] ProductEntity productEntity)  
      49.         {  
      50.             return _productServices.CreateProduct(productEntity);  
      51.         }  
      52.   
      53.         // PUT api/product/5  
      54.         public bool Put(int id, [FromBody] ProductEntity productEntity)  
      55.         {  
      56.             if (id > 0)  
      57.             {  
      58.                 return _productServices.UpdateProduct(id, productEntity);  
      59.             }  
      60.             return false;  
      61.         }  
      62.   
      63.         // DELETE api/product/5  
      64.         public bool Delete(int id)  
      65.         {  
      66.             if (id > 0)  
      67.                 return _productServices.DeleteProduct(id);  
      68.             return false;  
      69.         }  
      70.     }  
      71. }  
      Where we have a controller named Product and Action names as Verbs. When we run the application, we will get the following types of endpoints only. (Please ignore the port and localhost settings. It's because I run this application from my local environment.)

      controller

      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:

      1. using AttributeRouting;    
      2. using AttributeRouting.Web.Http;  

      Step 2

      Decorate your action with different routes:

      action

      As in the preceding image, I defined a route with the name productid that takse id as a parameter. We also need to provide a verb (GET, POST, PUT, DELETE or PATCH) along with the route as shown in the 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 the /help page, I will get the following:

      application

      In other words I got one more route for getting the product by id. When you 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.

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

      Therefore, we can have our custom route names as well as multiple endpoints for a single Action. That's exciting. Each endpoint will be different but will serve the same set of results.
      • {id?} : here "?" means that the parameter can be optional.

      • [GET("myproduct/{id:range(1, 3)}")], signifies that the product ids falling in this range will only be shown.

      More Routing Constraints

      You can leverage numerous Routing Constraints provided by Attribute Routing. I will provide an example for some of them.

      Range

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

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

      You can use it well for text/string parameters more efficiently.
      1. [GET(@"id/{e:regex(^[0-9]$)}")]  
      2. public HttpResponseMessage Get(int id)  
      3. {  
      4.     var product = _productServices.GetProductById(id);  
      5.     if (product != null)  
      6.         return Request.CreateResponse(HttpStatusCode.OK, product);  
      7.     return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No product found for this id");  
      8. }  
      for example [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's details from the database with his name as in the following:
      1. [GET("employee/name/{firstname}/{lastname?}")]  
      2. public string GetEmployeeName(string firstname, string lastname=”mittal”)  
      3. {  
      4.    …………….  
      5.   ……………….  
      6. }  
      In the preceding code, I marked the last name as optional using a question mark "?" to fetch the employee detail. It's my end-user's choice whether to provide the last name or not.

      So the preceding endpoint could be accessed using the GET verb with URLs as in the following:

        ~/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 preceding example, I marked "lastname" as an optional one and so provided a default value in the method parameter. If the user doesn't send a value then “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 on your own. Use the attribute [DefaultRoute] to define the default route values.

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

      I marked my actions as:

      1. // GET api/product  
      2. [GET("allproducts")]  
      3. [GET("all")]  
      4. public HttpResponseMessage Get()  
      5. {  
      6.     var products = _productServices.GetAllProducts();  
      7.     var productEntities = products as List<ProductEntity> ?? products.ToList();  
      8.     if (productEntities.Any())  
      9.         return Request.CreateResponse(HttpStatusCode.OK, productEntities);  
      10.     return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Products not found");  
      11. }  
      12.   
      13. // GET api/product/5  
      14. [GET("productid/{id?}")]  
      15. [GET("particularproduct/{id?}")]  
      16. [GET("myproduct/{id:range(1, 3)}")]  
      17. public HttpResponseMessage Get(int id)  
      18. {  
      19.     var product = _productServices.GetProductById(id);  
      20.     if (product != null)  
      21.         return Request.CreateResponse(HttpStatusCode.OK, product);  
      22.     return Request.CreateErrorResponse(HttpStatusCode.NotFound, "No product found for this id");  
      23. }  
      24.   
      25. // POST api/product  
      26. [POST("Create")]  
      27. [POST("Register")]  
      28. public int Post([FromBody] ProductEntity productEntity)  
      29. {  
      30.     return _productServices.CreateProduct(productEntity);  
      31. }  
      32.   
      33. // PUT api/product/5  
      34. [PUT("Update/productid/{id}")]  
      35. [PUT("Modify/productid/{id}")]  
      36. public bool Put(int id, [FromBody] ProductEntity productEntity)  
      37. {  
      38.     if (id > 0)  
      39.     {  
      40.         return _productServices.UpdateProduct(id, productEntity);  
      41.     }  
      42.     return false;  
      43. }  
      44.   
      45. // DELETE api/product/5  
      46. [DELETE("remove/productid/{id}")]  
      47. [DELETE("clear/productid/{id}")]  
      48. [PUT("delete/productid/{id}")]  
      49. public bool Delete(int id)  
      50. {  
      51.     if (id > 0)  
      52.         return _productServices.DeleteProduct(id);  
      53.     return false;  
      54. }  
      And therefore get the routes as in the following.

      GET:

      product

      POST / PUT / DELETE:

      POST PUT DELETE

      Check for more constraints here.

      You must be seeing “v1/Products” in every route, that is due to the RoutePrefix I used at the controller level. Let's explain RoutePrefix in detail.

      RoutePrefix: Routing at Controller level

      We were marking our actions with a specific route, but guess what? We can mark our controllers too with certain route names, we can do this using the RoutePrefix attribute of AttributeRouting. Our controller was named Product and I wanted to append Products/Product before my every action, therefore without duplicating the code at each and every action, I can decorate my Controller class with this name as in the following:
      1. [RoutePrefix("Products/Product")]  
      2. public class ProductController : ApiController  
      3. {  
      Now, since our controller is marked with this route, it will append that to every action too. For example, the route of the following action:
      1. [GET("allproducts")]  
      2. [GET("all")]  
      3. public HttpResponseMessage Get()  
      4. {  
      5.     var products = _productServices.GetAllProducts();  
      6.     var productEntities = products as List<ProductEntity> ?? products.ToList();  
      7.     if (productEntities.Any())  
      8.         return Request.CreateResponse(HttpStatusCode.OK, productEntities);  
      9.     return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Products not found");  
      10. }  
      Now becomes:

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

      RoutePrefix: Versioning

      A Route prefix can also be used for versioning of the endpoints, like in my code I provided “v1” as the version in my RoutePrefix as shown in the following:

      1. [RoutePrefix("v1/Products/Product")]  
      2. public class ProductController : ApiController  
      3. {  
      4. }  
      Therefore “v1” will be appended to every route / endpoint of the service. When we release the next version, we can certainly maintain a change log separately and mark the endpoint as “v2” at the controller level, that will append “v2” to all actions.

      For example:

        ~/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 where we do not want to use RoutePrefix for each and every action. AttributeRouting provides such flexibility too, that despite of a RoutePrefix present at the controller level, an individual action could have its own route too. It just needs to override the default route as in the following:

      • RoutePrefix at Controller:
        1. [RoutePrefix("v1/Products/Product")]  
        2. public class ProductController : ApiController  
        3. {  
      • Independent Route of Action:
        1. [Route("~/MyRoute/allproducts")]  
        2.  public HttpResponseMessage Get()  
        3.  {  
        4.      var products = _productServices.GetAllProducts();  
        5.      var productEntities = products as List<ProductEntity> ?? products.ToList();  
        6.      if (productEntities.Any())  
        7.          return Request.CreateResponse(HttpStatusCode.OK, productEntities);  
        8.      return Request.CreateErrorResponse(HttpStatusCode.NotFound, "Products not found");  
        9.  }  

      Disable Default Routes

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

      1. //config.Routes.MapHttpRoute(  
      2. // name: "DefaultApi",  
      3. // routeTemplate: "api/{controller}/{id}",  
      4. // defaults: new { id = RouteParameter.Optional }  
      5. //);  
      You can also remove the complete Register method, but for that you need to remove it calling too from the Global.asax file.

      Running the application

      Just run the application, we will get the following:

      Running the application

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

      WebAPITestClient

      You'll get “A simple Test Client for ASP.NET Web API”, just add it. You'll get a help controller in Areas -> HelpPage as in the following:

      HelpPage

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

      Append “/help” in the application URL and you'll get the test client.

      GET:

      GET

      POST:

      POST

      PUT:

      PUT

      DELETE:

      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 specific service. On that page there is a button Test API in the bottom-right corner, just press that button to test your service.

      test

      The following is the service for getting all the products:

      Service

      Get All products

      Likewise you can test all the service endpoints.

      pan
      Image source: rsc and guybutlerphotography

      Conclusion

      We now know how to define our custom endpoints and what their benefits are. Just to share that this library was introduced by Tim Call, author and Microsoft has included this in WebAPI 2 by default. My next article will explain token-based authentication using ActionFilters in the WepAPI. Until then, Happy Coding. You can also download the source code from GitHub. Add the required packages if they are missing in the source code.
      Click Download Complete Source Code for the source code with packages.

      References 

      Read more:

      For more technical articles you can reach out to CodeTeddy

      My other series of articles:

      Up Next
        Ebook Download
        View all
        Learn
        View all