Web API Routing

by Sachin Singh


Posted on Thursday, 11 June 2020

Tags: Web API routing Convention based routing in web api attribute routing in web api

Before understanding ,what is routing ,let me explain you why even the concept of routing has been introduced , and the simplest reason that comes in my mind is ,it enables you define URLs of your services and web pages.

Let's discuss some points regarding URLs.

How the URLs of a web application should look like.

See the below URLs of a blog site :-
  1. Home/Articles/3/4
  2. articles/csharp/array
  • obviously the first one is technical and not user-friendly, also it is not informative, on the other hand, the other one is user-friendly, well structured, and informative, anybody can tell only by seeing the URL about the content they are going to get with this request. Nowadays people like to share the link if they liked it over various social platforms and believe me a user-friendly URL gets more exposure.
  • second, it boosts your SEO, it makes sense that if your page's URL contains keywords and your page content really describes those keywords, then chances of ranking well in the search engines becomes higher.

How the URLs of a REST service should be

Let me explain some concepts which you will find unrelated at a first glance but at the end of this article, you will be able to relate everything easily.

1. GetAllStudent is a verb, as it is pointing to an action, which does something. Students are noun, as it is pointing towards a group of students. Now remember the Rest Principles
  • Everything on the web is a resource.
  • Each resource has a URL.

Now, use common sense and say which of the below URL belongs to a rest service.
  1. http://myapp/api/student/GetStudentCourse/2 or
  2.http://myapp/api/students/2/course

Obviously, the second URL is better than the first one from the "REST" point of view, because it is pointing to a resource on the server while the first one is action based which is against the Rest principle. So, the rule of thumb is if you are creating a restful service then your URL will have noun segments and not a verb.

2. Path parameters (routed data) are used to identify a resource on the server while query strings are used to sort or filter a resource. So, until you don't want filtering never use query string parameters in the URL, in other words, a rest service will not have a URL with query string parameters unless it is filtering or sorting a resource.
for example- http://api/students/1, is a good URL than http://api/students?Id=1, from Rest point of view, as the URL is pointing towards a particular student resource not filtering the resource based on Id. But http://api/students/2/courses?category=software, is a good URL even from the rest point, as it is actually filtering the courses taken by a particular student based on some category. obviously, anyone can debate on this, but this is what I think a URL should look like for a rest service.

so, now we know how the URLs of a rest service should look like, let's try to achieve the same, obviously the first step would be creating a Web API project and create any service as you wish, I am going to create a Student service where there will be two methods one which will expose students data and other will expose the list of courses taken by a particular student.

Step 1.Create two model classes , Student and Course.


   public class Student
    {
        public int Id { get; set; }
        public string Name { get; set; }

        public List<Course> Courses { get; set; }
    }

   public class Course
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

Step 2.Create a Controller and Name it StudentService and Create 3 methods as shown below.


   public class StudentServiceController : ApiController
    {

        public List<Student> LoadStudentData()
        {
            List<Student> students = new List<Student>()
            {
                new Student(){Id=1,Name="Sachin",Courses=new List<Course>(){new Course(){Id=1,Name="CSharp"},new Course() 
    {Id=2,Name="Asp.Net"}}},
                 new Student(){Id=1,Name="Vikash",Courses=new List<Course>(){new Course(){Id=1,Name="CSharp"},new Course() 
     {Id=2,Name="Asp.Net"},new Course(){Id=3,Name="KendoUI"}}},
                  new Student(){Id=1,Name="Arjun",Courses=new List<Course>(){new Course(){Id=1,Name="CSharp"},new Course() 
     {Id=4,Name="Angular"}}},
            };
            return students;
        }
        public HttpResponseMessage GetAllStudent()
        {
            try
            {
                var students= LoadStudentData();
                if(students!=null){
                    return Request.CreateResponse(HttpStatusCode.OK, students);
                }
                return Request.CreateErrorResponse(HttpStatusCode.NotFound,"no student found");
            }
            catch
            {
                return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, "Error");
            }
        }
        public HttpResponseMessage GetStudentCourses(string studentName)
        {
            try
            {
                var student = LoadStudentData().Where(s=>s.Name.ToLower()==studentName.ToLower()).SingleOrDefault();
              
                if (student != null)
                {
                    var courses = student.Courses.ToList();
                    return Request.CreateResponse(HttpStatusCode.OK, courses);
                }
                return Request.CreateErrorResponse(HttpStatusCode.NotFound, "no student found");
            }
            catch
            {
                return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, "Error");
            }
        }
     }

At this point our service is ready , now we want the following URLs for our service.
  1. All Student :- http://localhost:12345/api/Students
  2. Course taken by a student:- http://localhost:12345/api/students/{studentName}/courses

Now , the challenge that comes is , how to tell the Web API that if either of the above requests come then map it to appropriate handlers meaning if the request is http://localhost:12345/api/Students then invoke the GetAllStudent() action of StudentService controller and if the request is http://localhost:12345/api/students/{studentName}/courses , then invoke GetStudentCourses() action of StudentService controller.

And this is the place where exactly the Routing comes into the picture. Routing can be considered as a pattern matching system that matches the incoming http request's url pattern with all the url patterns stored in route collection and then maps that http request to appropriate controller Action. When the application launches then application registers one or more URL patterns with the framework's route table to tell the routing engine what to do with any requests that matches those patterns.

Routing involes the following three steps
  • Step 1: Finding the exact Route Template
  • Step 2: Finding the Controller
  • Step 3: Finding the Action Method

1. Finding the exact Route Template

Here you must be thinking, what exactly is the route template? You can think of Route template as a URI that consists of placeholders.

Example: "API/{controller}/{id}"

The above example looks like a normal URL the only difference is it is having placeholders for controller and id. These placeholders are kept within curly braces.

Note:
  • Route Template can also contain literals, like "API/{controller}/{id}". Here the word "API" is literal.
  • Placeholder's value can also be given in the defaults of the Route. Like defaults: new { Controller="Employee" }.

Explanation

Route Template acts as an entry point for the Routing in ASP.NET WEB API. The Web API looks for the controller and action method if the requested URL matches with any of the registered URI in the form of a route template.

When a Request arrives at the API, first of all, the web API tries to match the URI with one of the Route Templates. If the template contains literals, then the API checks for an exact match of characters. And placeholders are replaced with the dynamic value from the URL. In the above example, the Route Template has 'API' as literal, API checks the same literal in the requested URL.

Note:
  • API does not check URI's hostname.
  • By default the Web API selects the very first route which is matched with the route template.
  • Web API doesn't search for the placeholder's value if they are already provided in the defaults.

Example: URI: http://localhost/API/1
The above URI match with the defined Route Template as Controller placeholder value has already been provided in the default of the Route.
Once the Requested URI is matched with Route Template, the placeholder's values are stored as the Key-value pair in Route Dictionary, where Keys are the placeholders and their values are taken from the URI.
Based on the above Route Template, Route Dictionary will have the following values
"Controller: Employee" and "id:1".

2. Finding the Controller

Finding the controller is simple. We know that the Route Dictionary stores the value of the controller which it gets from the URL, Web API gets the controller's value from this dictionary by providing the key, where the key is simply the word 'controller'. In the above example, the Controller's value is Employee. This value is been taken as the Controller key has Employee value. At last, the framework appends the Controller string to the value and Search for that controller. i.e EmployeeController. If there is no match for this controller then API returns an error message.

3. Finding the Action Method

This is a little typical phase of Routing. To find the exact action method, the framework looks for the three things, They are:
  • The HTTP Method of the Request.
  • Action placeholder value from the route dictionary.
  • Parameters of the actions.

Here, the HTTP Method has an important role in selecting the exact action method. An important aspect is that the Action methods are selected based on the Naming Convention of the HTTP method or Attribute placed with the HTTP Method above it.

Explanation:
  • If the request is a Get request, then either Action name should be Get or it should start with the word "Get___"
Example: GetEmployee() or GetEmployeeDetails() actions will be selected.
  • The Action method selection will be made on the attribute as well.


  [HttpGet]  
   public HttpResponseMessage ShowEmployee()  
    {  
           //To Do  
    }  

Note:
  • The Framework only looks for the public methods which are inherited from ApiController class. If the method is private or not inherited by the APIController, framework doesn't consider them as API methods and returns an error.
  • If an Action Method does not satisfy the naming convention nor it contains any HTTP verb attribute, then, by default the framework treats the action method as the HttpPost method. (In case the Controller has specified action name which matches the exact action key value of Route Dictionary.)

Now you know what is routing and Why routing is necessary let's come to our requirement, our student service is ready and we want the following URLs for our service
  1. All Student :- http://localhost:12345/api/Students
  2. Course taken by a student:= http://localhost:12345/api/students/{studentName}/courses

Now, what do you think, our first step should be? obviously, we have to define the Route templates according to the URL structure we want and add them to the framework's Route table. Now, the question that arises is, where to define the route template and how to add it to the RouteCollection (route table), as we had RouteConfig class in MVC to define route templates, in Web API we have WebApiConfig class which has a static Register method which accepts HttpConfiguration instance, the HttpConfiguration class has a public property "Routes" of type HttpRouteCollection, which is extended to add various route templates, in short MapHttpRoute() is an extension method of RouteCollection (routes) where we define route templates like shown below.


   public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API configuration and services

            // Web API routes
            config.MapHttpAttributeRoutes();
            config.Routes.MapHttpRoute(
               name: "AllStudent",
               routeTemplate: "api/students",
               defaults: new { controller = "StudentService" }
           );
            config.Routes.MapHttpRoute(
              name: "StudentCourse",
              routeTemplate: "api/students/{StudentName}/courses",
              defaults: new { controller = "StudentService" }
          );

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

Explanation:-
1. To get all students:- whenever a request comes that matches the URL template "api/students" the framework gets the controller's value from the default and save it to the route dictionary in key-value pair, the SelectController method gets the controller value from the dictionary and appends Controller string to it and invoke that controller, as the HTTP method is a get, the framework tries to invoke the method which starts with "Get" word, in our case both the method starts with the "Get" word but one is parameterless and other is parameterized, as the dictionary does not have any Key value pair for parameterized method, the parameterless method is invoked.
2. To get a student course:- whenever a request comes that matches the URL template "api/students/{StudentName}/courses" the framework gets the controller's value from the default and StudentName from the URL and save it to the route dictionary in key-value pair, notice here the dictionary also contains the StudentName, the SelectController method gets the controller value from the dictionary and appends Controller string to it and invoke that controller, as the HTTP method is a "Get" method, the framework tries to invoke the method which starts with the word "Get", in our case both the method starts with the word "Get" but one is parameterless and other is parameterized, as the dictionary does have StudentName: something as Key-value pair for parameter binding, the parameterized method is invoked.

Different ways of defining routes in Web API:-

Web API supports two types of routing:-
1.Convention based routing and
2.Attribute routing.

• Convention based routing is very much similar to MVC routing, here we define one or more route templates, which are basically parameterized strings, at one place that is in the WebApiConfig class. When the framework receives a request, it matches the URI against the route template, The only difference is that Web API uses the HTTP verb, not the URI path, to select the action. But we can also use MVC-style routing in Web API and can provide Action name in the route template, though from the "REST" point of view it is not recommended. In the above example, we have used convention-based routing.
• convention-based routing makes it difficult to achieve certain URI patterns that are common in RESTful services. For example, resources often contain child resources like Customers may contain a list of orders, movies have actors, books have authors, students have courses, and so forth. It's natural to create URIs that reflect these relations: students/1/courses. This type of URI pattern is very difficult to achieve using convention-based routing. even if it could be achieved, the results don't scale well if you have many controllers multi-levels of resources. That is why Web API 2 supports a new type of routing, called attribute routing. As the name implies, we use attributes to define routes. with Attribute routing we have more control over the URL patterns For example, we can easily create URIs that describe hierarchies of resources as shown in the below example.


   [Route("api/students/{StudentName}/courses")]
        public HttpResponseMessage GetStudentCourses(string studentName)
        {
            try
            {
                var student = LoadStudentData().Where(s=>s.Name.ToLower()==studentName.ToLower()).SingleOrDefault();
              
                if (student != null)
                {
                    var courses = student.Courses.ToList();
                    return Request.CreateResponse(HttpStatusCode.OK, courses);
                }
                return Request.CreateErrorResponse(HttpStatusCode.NotFound, "no student found");
            }
            catch
            {
                return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, "Error");
            }
        }

Note:- In order to use attribute routing with Web API, we need to enable it in WebApiConfig.cs file by calling config.MapHttpAttributeRoutes() method.

At last, if anything is defined it must be called from somewhere in the application, we know the routes should be stored in the routing table before any requests hit the Web API, meaning when the application launches for the very first time the route templates should be added to the routing table, so the best place to call the Register method of WebApiConfig class is the application_Start event of global.asax as shown below.


       protected void Application_Start()
        {
            GlobalConfiguration.Configure(WebApiConfig.Register);
        }

Now , when you run the application you get the desired results when request is made through appropriate URLs.

Courses taken by a student
Courses taken by a student
All students list
Details of all students