CHAPTER 8

image

Routing

In our ASP.NET Web API applications, incoming HTTP requests are handled by controller action methods. When an incoming request is received, it is routed to a proper action method, and the routing system provides the fundamental layer for this movement.

The routing feature in ASP.NET Web API is one of the key components of the whole framework. The HTTP request is being retrieved by the routing system, and this routing system differs with the choice of the hosting environment (ASP.NET Host, Self-Host, etc.) There are a lot of implementation details involved in this process. We will go through them lightly rather than in depth, because one of this book’s aims is to avoid too much talk about implementation details. Instead, we will look at the routing feature from the API standpoint, particularly at how that feature behaves and how to take control of it.

Understanding Routing Mechanism

Throughout the book, we generally try to avoid mentioning the framework’s implementation details,  but we think that going a little further inside the processing pipeline will help you understand how routing gets involved and where it is being used. The fundamental goal of this section is to show you that there is nothing magical about the routing mechanism by giving you a little bit of an inside scoop on its workings.

If you have ever played with ASP.NET MVC, the routing feature in ASP.NET Web API will seem very familiar because the concept is the same. ASP.NET routing was first introduced by ASP.NET MVC before .NET v4.0 in a separate, bin-deployable assembly named System.Web.Routing.dll. The main idea behind the routing mechanism was to enable request-dispatching by providing a certain URL with an HTTP handler that could process the requests made to that URL. These routes can be registered through the Add method of RouteTable.Routes static property, and this method accepts two parameters: the name of the route as a string and the route item as System.Web.Routing.RouteBase. In order to inject the routing mechanism into the request processing pipeline, an HTTP module named System.Web.Routing.UrlRoutingModule was introduced inside the same assembly. With the introduction of .NET v4.0, this module is registered by default when an application is run under .NET Framework v4.0 Integrated Application Pool in IIS.

You might be thinking that running an ASP.NET Web API application under IIS is not the only option and that we don’t have a routing module in other hosting options, such as self-host. So what happens then? This is the key difference in the ASP.NET Web API routing system. The routing feature has been decoupled from the ASP.NET routing mechanism in ASP.NET Web API so that the same configuration model can be used regardless of the hosting choice.

Let’s see a simple example of registering a route when an application is being hosted inside the IIS (Listing 8-1).

Listing 8-1.  Route Registration in ASP.NET Web API with ASP.NET Hosting

protected void Application_Start(object sender, EventArgs e) {

    GlobalConfiguration.Configuration.Routes.MapHttpRoute(
        "DefaultHttpRoute",
        "api/{controller}/{id}",
        new { id = RouteParameter.Optional }
    );
}

When using the ASP.NET hosting option, routes are registered through the MapHttpRoute extension method of the static Routes property (a type of HttpRouteCollection) of the HttpConfiguration object, which can be reached through the GlobalConfiguration.Configuration property. If you are in a self-host environment, use the same MapHttpRoute extension method of HttpRouteCollection, but this time you will reach HttpRouteCollection through the Routes property of the HttpSelfHostConfiguration instance. Listing 8-2 shows how to register the route shown in Listing 8-1 but in a self-host environment. (Refer to Chapter 17 for more information about hosting options.)

Listing 8-2.  Route Registration in ASP.NET Web API with Self-Host

class Program {

    static void Main(string[] args) {

        var config =
            new HttpSelfHostConfiguration(
                new Uri("http://localhost:5478"));

        config.Routes.MapHttpRoute(
            "DefaultHttpRoute",
            "api/{controller}/{id}",
            new { id = RouteParameter.Optional }
        );

        //Lines omitted for brevity
    }
}

In both scenarios, the MapHttpRoute extension method of HttpRouteCollection creates a System.Web.Http.Routing.IHttpRoute instance and adds it to the HttpRouteCollection. This IHttpRoute instance varies according to the hosting choice. For example, on the ASP.NET host, the System.Web.Http.WebHost.Routing.HostedHttpRoute internal class, which is one of the implementations of IHttpRoute, is being used. On the other side, in a self-host environment, System.Web.Http.Routing.HttpRoute, which is the route class for self-host (i.e., hosted outside of ASP.NET), is being used.

Finally, depending on the hosting choice, the routes are inspected and the proper actions take place if there are any matching routes. With ASP.NET hosting, a System.Web.Http.WebHost.HttpControllerRouteHandler instance is attached to every single route. This HttpControllerRouteHandler instance is an implementation of System.Web.Routing.IRouteHandler, which is responsible for returning a System.Web.IHttpHandler implementation to process the incoming request. In the case here, the handler is the System.Web.Http.WebHost.HttpControllerHandler, which processes the request by gluing together all the components of the ASP.NET Web API. In the case of self-host, the WCF channel stack and the service model layers are involved in receiving and processing the request.

The beauty here is that we don’t have to concern ourselves about any of this when building Web API applications, but it never hurts to know what is going on under the hood or get a bit of a sense of how the system works. From now on, we will only talk about routing by giving samples with ASP.NET Web hosting. Except for the registration point, all the other features will work the same in a self-hosting environment.

Defining Web API Routes

In ASP.NET Web API, routes need to be defined on the System.Web.Http.GlobalConfiguration.Configuration.Routes property with the MapHttpRoute extension method, as shown in the previous section. Actually, there are other ways to register routes—the Add method of HttpRouteCollection class, for example. This method accepts two parameters: a string parameter for the name of the route and IHttpRoute implementation. Also, routes can be directly added into the System.Web.Routing.RouteTable.Routes static property with the MapHttpRoute extension method if the application is hosted under ASP.NET. We’ll use the Routes static property on the GlobalConfiguration.Configuration object for registering routes, along with the MapHttpRoute extension method, throughout this chapter.

Listing 8-3 is an example of a registered route for ASP.NET Web API.

Listing 8-3.  Sample Route Registration

protected void Application_Start(object sender, EventArgs e) {

    GlobalConfiguration.Configuration.Routes.MapHttpRoute(
        "DefaultHttpRoute",
        "api/{controller}/{id}",
        new { id = RouteParameter.Optional }
    );
}

Notice that the routes are registered inside the Application_Start method in the Global.asax file (actually, in full it’s the Global.asax.cs file). Before explaining the route just defined, let’s see the overloads of the MapHttpRoute extension method (Listing 8-4).

Listing 8-4.  MapHttpRoute Extension Method Overloads

public static IHttpRoute MapHttpRoute(
    this HttpRouteCollection routes,
    string name,
    string routeTemplate);

public static IHttpRoute MapHttpRoute(
    this HttpRouteCollection routes,
    string name,
    string routeTemplate,
    object defaults);

public static IHttpRoute MapHttpRoute(
    this HttpRouteCollection routes,
    string name,
    string routeTemplate,
    object defaults,
    object constraints);

public static IHttpRoute MapHttpRoute(
    this HttpRouteCollection routes,
    string name,
    string routeTemplate,
    object defaults,
    object constraints,
    HttpMessageHandler handler);

In our example we used the first overload method shown in Listing 8-4. The first string parameter given here is the name of the route. The route name can be any arbitrary string, but if there are multiple routes, the name of each route should be unique. The second parameter passed is the route template, which will be compared with the URL of the incoming request. As a third parameter, an anonymous object for default values has been passed. If the Optional static property type of RouteParameter is passed for a route parameter, that route parameter can be omitted. As a fourth parameter, another anonymous object can be passed to specify the route constraints, which will be covered in the “Route Constraints” subsection. Finally, the last overload of the MapHttpRoute method accepts five parameters, and this overload is for per-route message handlers which we will see in Chapter 10.

image Note  If your ASP.NET Web API application is running under ASP.NET side by side with any other ASP.NET framework, such as ASP.NET MVC or ASP.NET Web Forms, all of your routes get collected inside the System.Web.Routing.RouteTable.Routes static property, regardless of the framework type. So each route needs a unique name. Otherwise you will get an error indicating that a route with the specified name is already in the route collection. For example, you cannot have both a Web API route and an MVC route named MyRoute inside the same application.

HTTP Methods and Routing

In routing with ASP.NET Web API, the HTTP methods that the incoming request is made through, virtually invisible at first glance, play a huge role in the selection of controller actions. Routing in ASP.NET Web API is inspired by ASP.NET MVC routing, but there is one missing part inside the example in Listing 8-3: the action route parameter. For those not familiar with ASP.NET MVC, the action parameter represents the ASP.NET MVC controller action method name and the action method inside the controller is selected on the basis of this parameter’s value. In ASP.NET Web API, the actions selected depend upon the HTTP method the request is made through.

For example, there is a sample controller named CarsController, as shown in Listing 8-5.

Listing 8-5.  Sample CarsController

public class CarsController : ApiController {

    public string[] Get() {

        return new string[] {

            "Car 1",
            "Car 2",
            "Car 3",
            "Car 4"
        };
    }
    public string Get(int id) {

        return string.Format("Car {0}", id);
    }

    public string[] GetCarsByType(string type) {

        return new string[] {

            "Car 2",
            "Car 4"
        };
    }
    public string[] GetCarsByMake(string make) {

        return new string[] {
            "Car 1",
            "Car 3",
            "Car 4"
        };
    }
    public string[] GetCarsByMakeByType(
        string make, string type) {

        return new string[] {
            "Car 4"
        };
    }
}

With the route registered in Listing 8-3, the first CarsController.Get method in Listing 8-5 will be invoked when there is a GET request to the /api/cars URL. Let’s quickly go through the steps to understand how the Get method of CarsController is invoked.

The “api” part inside the URL is the static URL segment that was specified inside the route template. The second segment inside the URL is “cars”. Because it was indicated inside the route template that the second segment would be the controller name, the system will look for a unique class that implements the IHttpController interface and has a prefix of “cars” (case insensitive) and “Controller” (again case insensitive), as concatenated on the prefix (CarsController). This action is performed by the registered implementation of the IHttpControllerSelector interface, which is the DefaultHttpControllerSelector class by default and the controller selector instance here is invoked by HttpControllerDispatcher. If any controller is found, the request goes up one level inside the pipeline. If no controller is found, the HttpControllerDispatcher sets up the “404 Not Found” response message and terminates the request. The example here has a controller named CarsController and this means that the request will be dispatched to a higher level inside the pipeline, where the execution of the ExecuteAsync method of the controller instance happens.

image Note  Until now, we have used ApiController in all examples in this book as a base controller class and the ApiController as the default implementation of IHttpController interface, which brings filters and a lot of useful functionality. The controller action selection logic that will be briefly explained in the next paragraphs is applicable only if the ApiController is used as the base controller class along with the ApiControllerActionSelector, which is the default implementation of IHttpActionSelector. If you want to replace the ApiController with another implementation of IHttpController, there won’t be any default action methods. The controllers and action selection logic will be gone through in depth in Chapter 9, so that part will only be touched upon here.

In the example, when the ExecuteAsync method of the ApiController class is executed, the action method selection logic kicks in. (The details of how this logic is performed mostly belong to Chapter 9, where this will be covered in depth.) What you need to know here to make sense out of the current scenario is that the action method selection occurs on the basis of the HTTP method chosen. For example, here a GET request is being made to the api/cars URL, and it is dispatched to CarsController. As the request is made through the HTTP GET method, the controller will look for a method with a prefix of “Get” (case insensitive).

One question that may pop up right now might concern having two Get methods inside CarsController. As one of the Get methods accepts the parameter id and the request doesn’t contain an id value (such as a route or a query string value), the system will select the method with no parameters. Trying to make a GET request to the URL api/cars/1 will invoke the second Get method because the third URL segment inside the route template represents the id value. This value is an optional parameter because its default value was defined as System.Web.Http.RouteParameter.Optional. Table 8-1 shows a list of request URLs and HTTP methods and their corresponding action methods.

Table 8-1. Possible Request Options for the Sample in Listing 8-5 with the Route in Listing 8-3

URL HTTP Method Corresponded Action Method
/api/cars GET Get()
/api/cars?foo = bar GET Get()
/api/cars/1 GET Get(int id)
/api/cars?type = SUV GET GetCarsByType(string type)
/api/cars?make = make1 GET GetCarsByMake(string make)
/api/cars?make = make1&type = SUV GET GetCarsByMakeByType(string make, string type)

This behavior is the same for other verbs as well (as indicated earlier, it will be explored in depth in Chapter 9). Another option with routing—also known as RPC, or Remote Procedure Call—enables the direct calling of an action method, for example, by issuing a GET request to /api/cars/ExpensiveCars and invoking the ExpensiveCars method inside the CarsController. This behavior mostly relates to the controller part of the framework, so it too will be thoroughly explored in Chapter 9.

Having Multiple Web API Routes

Depending on your needs, you might want to have multiple Web API routes in your application. This is a totally acceptable option in ASP.NET Web API, but there are a few important things to be aware of.

If you have multiple routes, the registration order of the routes matters. When a request comes to the routing level, the route collection is scanned to find a match. As soon as a match is found, the search stops, and the remaining routes get ignored. The first route registered will be looked at first, and so on. Let’s see the example in Listing 8-6.

Listing 8-6.  Multiple Routes Sample

protected void Application_Start(object sender, EventArgs e) {

    var routes = GlobalConfiguration.Configuration.Routes;

    routes.MapHttpRoute(
        "DefaultHttpRoute",
        "api/{controller}/{id}",
        new { id = RouteParameter.Optional }
    );
    routes.MapHttpRoute(
        "VehicleHttpRoute",
        "api/{vehicletype}/{controller}",
        defaults: new { },
        constraints: new { controller = "^vehicles$" }
    );
}

The first route is the route that has been used so far in this chapter, and the second one is a little different. Notice that a constraint was used for the controller parameter (don’t worry, constraints will be explained later in this chapter). With this constraint in place, we are indicating the routing mechanism to match only if the controller parameter is “vehicles”. To complete the sample, there are also two controllers: CarsController (Listing 8-7) and VehiclesController (Listing 8-8).

Listing 8-7.  CarsController

public class CarsController : ApiController {

    public string[] Get() {

        return new string[] {
            "Car 1",
            "Car 2",
            "Car 3"
        };
    }
}

Listing 8-8.  VehiclesController

public class VehiclesController : ApiController {

    public string[] Get(string vehicletype) {

        return new string[] {
            string.Format("Vehicle 1 ({0})", vehicletype),
            string.Format("Vehicle 2 ({0})", vehicletype),
            string.Format("Vehicle 3 ({0})", vehicletype),
        };
    }
}

In this case, if a request is sent against /api/SUV/vehicles to invoke the action method inside VehiclesController, the second route will never be hit because the first one will be a match and “SUV” will be the value of the controller parameter for the first route template. So the response will be a “404 Not Found”, as shown in Figure 8-1.

9781430247258_Fig08-01.jpg

Figure 8-1. GET request against /api/SUV/vehicles and 404 response

If the routes are registered as Listing 8-9 shows, you will get the exact behavior desired here (Figure 8-2).

Listing 8-9.  Multiple Routes Correct Sample

protected void Application_Start(object sender, EventArgs e) {

    var routes = GlobalConfiguration.Configuration.Routes;

    routes.MapHttpRoute(
        "VehicleHttpRoute",
        "api/{vehicletype}/{controller}",
        defaults: new { },
        constraints: new { controller = "^vehicles$" }
    );

    routes.MapHttpRoute(
        "DefaultHttpRoute",
        "api/{controller}/{id}",
        new { id = RouteParameter.Optional }
    );

}

9781430247258_Fig08-02.jpg

Figure 8-2. GET request against /api/SUV/vehicles and 200 response

Route Defaults

For a route to match a request, the first requirement is to supply all the parameter values. For example, if the route template consists of three parameters and only two of them are supplied, the route will be ignored unless the ignored parameters have a default value specified.

The route parameters can have default values and having them enables you to omit specifying parameters. Listing 8-10 shows a slightly different route from the one already used a lot in this chapter.

Listing 8-10.  A Route Sample

protected void Application_Start(object sender, EventArgs e) {

    var routes = GlobalConfiguration.Configuration.Routes;

    routes.MapHttpRoute(
        "DefaultHttpRoute",
        "api/{controller}/{id}"
    );
}

A look at the route template will show that the URL for this route consists of three segments. The first one is the static value, and the remaining two are the parameters that can be changed. When a request is sent to /api/cars/15, this route will be a match, and the request will get inside the ASP.NET Web API pipeline. Omit the id parameter and send a request against /api/cars, however, and this route will never be a match.

There are two solutions to this issue.

  • Specify another route, one that will be a match for the request.
  • Provide a default value for the id parameter to omit the id segment inside the URL.

Let’s take the second approach to solve the issue here. In order to provide default values for the parameters, let’s provide an anonymous object for the default parameter of the MapHttpRoute extension method. The property names of the anonymous object should be the same as the parameter names. If a property name provided doesn’t have a match among the route parameters, then that property will get ignored. Listing 8-11 is an example of a solution to the problem.

Listing 8-11.  A Route Sample with Default Values

protected void Application_Start(object sender, EventArgs e) {

    var routes = GlobalConfiguration.Configuration.Routes;

    routes.MapHttpRoute(
        "DefaultHttpRoute",
        "api/{controller}/{id}",
        new { id = string.Empty }
    );
}

When a request is now sent to /api/cars, this route will be a match, and the value of the id route parameter will be String.Empty, which is equivalent to “ ”. If a value is provided for the id segment, the default value will be overridden by the one provided. For example, if a request is sent to /api/cars/15, the value of the id route parameter will be 15.

Optional Parameters

What is actually wanted in the above example is, not a default value for the id parameter, but an optional parameter. In the Web API framework, provide the Optional static property of the System.Web.Http.RouteParameter for a route parameter as the default value, and the route parameter will be omitted. Listing 8-12 is an example of this feature.

Listing 8-12.  A Route with Optional Parameters

protected void Application_Start(object sender, EventArgs e) {

    var routes = GlobalConfiguration.Configuration.Routes;

    routes.MapHttpRoute(
        "DefaultHttpRoute",
        "api/{controller}/{id}",
        new { id = RouteParameter.Optional }
    );
}

When a request is sent to /api/cars, the route in Listing 8-12 will still be a match, but this time there will be no id parameter specified. Instead, the id parameter will be omitted.

Route Constraints

In some cases, you won’t want a particular URL structure to match a route. In such cases, the route constraint feature allows you to restrict the requests that match a particular route. A route constraint can be applied to each route parameter and in order to specify a constraint for a route parameter, you will need to provide an anonymous object for the constraints parameter of the MapHttpRoute extension method. The property names of the anonymous object provided should be the same as the parameter names.

The route constraints can be applied either as regular expressions or as custom route constraints, and these options will be explained separately.

Regular Expression Route Constraints

If you provide a string value as a route constraint object property value, the system will assume that it is a regular expression route constraint and process the constraint that way. Let’s assume that you’d like to receive id parameters as digits for a particular route. Listing 8-13 shows how to implement this using a regular expression route constraint.

Listing 8-13.  Regular Expression Route Constraint Sample

protected void Application_Start(object sender, EventArgs e) {

    var routes = GlobalConfiguration.Configuration.Routes;

    routes.MapHttpRoute(
        "DefaultHttpRoute",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { },
        constraints: new { id = @"d + " }
    );
}

By sending a request to /api/cars/15, this route will be a match, because the id parameter value consists only of digits. If a request is sent to /api/cars/foo, the route will not be a match, because “foo” value doesn’t match the digits-only regular expression specified.

Custom Route Constraints

In some cases, the route parameter value is not enough to see if the route should be a match or not, and custom route constraints come in handy in such cases. A custom route constraint is a class that implements the System.Web.Http.Routing.IHttpRouteConstraint interface. The IHttpRouteConstraint interface has only one method, called Match, which will return a Boolean value. If the return value is true, it means the route parameter is valid. If the return value is false, the route parameter is invalid.

Currently, the RouteParameter.Optional and regular expression route constraints don’t get along very well. For example, in the example shown in Listing 8.13, RouteParameter.Optional couldn’t be provided as the default value for the id parameter. Had we done that, all the requests that came to /api/cars would be rejected because the regular expression values would not match. Listing 8-14 shows a custom route constraint implementation which works around this problem.

Listing 8-14.  A Custom Route Constraint Implementation

public class OptionalRegExConstraint : IHttpRouteConstraint {

    private readonly string _regEx;

    public OptionalRegExConstraint(string expression) {

        if (string.IsNullOrEmpty(expression)) {

            throw new ArgumentNullException("expression");
        }
        _regEx = expression;
    }

    public bool Match(
        HttpRequestMessage request,
        IHttpRoute route,
        string parameterName,
        IDictionary <string, object> values,
        HttpRouteDirection routeDirection) {

        if (values[parameterName] != RouteParameter.Optional) {

            object value;
            values.TryGetValue(parameterName, out value);
            string pattern = "^(" + _regEx + ")$";
            string input = Convert.ToString(
                value, CultureInfo.InvariantCulture);

            return Regex.IsMatch(
                input,
                pattern,
                RegexOptions.IgnoreCase |
                RegexOptions.CultureInvariant);
        }

        return true;
    }
}

As you can see, all the information you need is being provided inside the Match method. You have access to the whole HttpRequestMessage and necessary route information. The OptionalRegExConstraint mimics the regular expression constraint functionality of the underlying routing system. Additionally, this custom route constraint also examines the route parameter’s value to see whether it is RouteParameter.Optional. If so, the route constraint just returns true indicating that a route parameter is a match.

Listing 8-15 shows the registration code for this custom constraint.

Listing 8-15.  Custom Route Constraint Registration

protected void Application_Start(object sender, EventArgs e) {

    var routes = GlobalConfiguration.Configuration.Routes;

    routes.MapHttpRoute(
        "DefaultHttpRoute",
        "api/{controller}/{id}",
        new { id = RouteParameter.Optional },
        new { id = new OptionalRegExConstraint(@"d + ") }
    );
}

Now when a request comes for /api/cars, the request will pass through the routing successfully, as the id parameter is optional (Figure 8-3).

9781430247258_Fig08-03.jpg

Figure 8-3. GET request against /api/cars

If a request is made to /api/cars/15, the route will still be a match, because the id segment of the URL consists only of digits (Figure 8-4).

9781430247258_Fig08-04.jpg

Figure 8-4. GET request against /api/cars/15

If we send a request to /api/cars/foo, the route won’t be a match because the id segment of the URL doesn’t consist of digits (Figure 8-5).

9781430247258_Fig08-05.jpg

Figure 8-5. GET request against /api/cars/foo

Summary

Routing is one of the key components of the ASP.NET Web API framework and the first place where the actual request arrives. This chapter first looked at implementation details and then moved forward to the actual usage and features of the routing mechanism.

Routing has an important place inside the framework for several reasons. One is that default controller and action selection logics heavily rely on routing mechanism. Also, another key feature of routing allows us to produce REST-friendly URIs.

If you are familiar with ASP.NET routing mechanism, the routing feature in ASP.NET Web API will feel very familiar to you. Actually, the ASP.NET routing module is being used under the hood when you are hosting your Web API application on ASP.NET, but the framework abstracts the underlying infrastructure away to make its hosting layer agnostic.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.107.85