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
- Range
- Regular Expression
- 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.
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
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.
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,
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Do not be confused by MVC routes, since we are using an MVC project we also get MVC routes defined in the RouteConfig.cs file as in the following.
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, the WebAPI route. 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 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.
public HttpResponseMessage Get()
public HttpResponseMessage Get(int id)
public int Post([FromBody] ProductEntity productEntity)
public bool Put(int id, [FromBody] ProductEntity productEntity)
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.
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 }
);
}
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 which 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 a route template like route template: "API/{controller}/{id}" or route template: "API/{controller}/{action}/{id}" or route template: "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 existing 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.
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.
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 for 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.
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 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.
We now need to set up our WebAPI project and define our routes.
Our existing ProductController class looks something as shown in the following.
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 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.)
- 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.
As in the preceding image, I defined a route with the name productid that takes 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(“my product/id/{id?}”)], and many more.
Now when I run the application and navigate to the /help page, I will get the following.
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 shown 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 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.
[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");
}
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.
[GET("employee/name/{firstname}/{lastname?}")]
public string GetEmployeeName(string firstname, string lastname = "mittal")
{
// …………….
// …………….
}
In the preceding code, I marked the last name as optional using a question mark "?" to fetch the employee details. 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 "last name" 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,
// 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;
}
Therefore get the routes as in the following.
GET
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.
[RoutePrefix("Products/Product")]
public class ProductController : ApiController
{
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.
[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.
A Route prefix can also be used for versioning of the endpoints, in my code I provided “v1” as the version in my RoutePrefix as shown in the following.
[RoutePrefix("v1/Products/Product")]
public class ProductController : ApiController
{
}
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, which 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 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
[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 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 the default route provided in the 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.
//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 it from calling from the Global.asax file.
Running the application
Just run the application, and we will get the following.
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 search box in online packages as in the following.
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.
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
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 specific service. On that page there is a button Test API in the bottom-right corner, just press that button to test your service.
The following is the service for getting all the products.
Likewise, you can test all the service endpoints.
Image source: RSC and Guy Butler Photography
Conclusion
We now know how to define our custom endpoints and what their benefits are. Just to share this library was introduced by Tim Call, the 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,