CHAPTER 21

image

URL Routing: Part II

In this chapter, I continue describing the Web API URL routing feature, focusing on direct routes, which are defined by applying attributes to controllers and action methods. I also show you different ways in which you can customize the routing process. Table 21-1 summarizes this chapter.

Table 21-1. Chapter Summary

Problem

Solution

Listing

Define a direct route.

Apply the Route attribute to one or more action methods or to the controller itself.

1, 2, 10, 11

Define a common prefix that will be used in all of the direct routes in a controller.

Apply the RoutePrefix attribute to the controller class.

3

Define an optional segment in a direct route.

Add a question mark to the segment name and define a default parameter name.

4, 5

Define a default segment in a direct route.

Assign a value to the segment in the route template.

6

Constrain a direct route.

Add a constraint shorthand to the segment in the route template.

7, 8, 16–20

Change the precedence of direct routes.

Set the Order property of the Route attribute.

9

Handle a request matched by a contention-based route without a controller.

Create a custom route handler.

12, 13

Pass information from the route to other components.

Use data tokens.

14, 15

Preparing the Example Project

I am going to carry on using the Dispatch project, but I am going to remove the custom routes that I added in Chapter 20 so that the application has only the default routing configuration defined in the WebApiConfig.cs file, as shown in Listing 21-1.

Listing 21-1. The Contents of the WebApiConfig.cs File

using System.Web.Http;

namespace Dispatch {
    public static class WebApiConfig {
        public static void Register(HttpConfiguration config) {

            config.MapHttpAttributeRoutes();

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

Removing the custom routes means that the client I added in Chapter 20 is unable to target the action methods in the Today controller because the default convention-based route doesn’t capture an action value and there are multiple actions that have been decorated with the HttpGet attribute. To see the effect of removing the custom routes, start the application and navigate to the /Home/Today URL using the browser. Clicking the Get Day button will produce a 500 (Internal Server Error) message, as shown in Figure 21-1.

9781484200865_Fig21-01.jpg

Figure 21-1. The effect of removing custom routesfrom the example application

Understanding Direct Routing

Direct routes are applied using attributes to the controller class and action methods, rather than in the WebApiConfig.cs file. Direct routing supports all of the same features as convention-based routing—including route templates, fixed and variable segments, defaults, and constraints—but they are applied directly to the controller class. In the sections that follow, I show you how to create direct routes and demonstrate how they work. Table 21-2 puts direct routing in context.

Table 21-2. Putting Direct Routing in Context

Question

Answer

What is it?

Direct routing allows routes to be defined by applying attributes to action methods or controller classes.

When should you use it?

See the “Selecting Convention-Based or Direct Routing” sidebar.

What do you need to know?

Features such as optional segments, default segment values, and segment constraints are all applied to the route template.

SELECTING CONVENTION-BASED OR DIRECT ROUTING

The difference between the two styles of routing is how individual routes are defined. As you saw in Chapter 20, convention-based routing puts all of the routes in the WebApiConfig.cs file. By contrast, direct routing defines routes through the use of attributes.

There is no technical difference in the routes that are created or the way that they are evaluated, and when it comes to choosing a style of routing, you should pick whichever one feels right to you. For me, this is convention-based routing because I like to keep the different parts of the application separate, but for many others, the attraction of direct routing is that you can see how routes relate to action methods by looking at the controller classes.

There are programmers who firmly believe that one approach to routing is superior to the other, but they are mistaking their preferences for a perceived benefit that doesn’t exist, regardless of which routing style they advocate. You can match any pattern of URLs using either technique, and you can safely ignore anyone who argues otherwise.

Don’t worry if you don’t have a preference for one style of routing. Web API allows convention-based and direct routing to coexist in an application, and you can easily experiment to see what works best for you. If you don’t know where to start, then I recommend you start with convention-based routing. If you find yourself staring blankly at the routes you end up with in the WebApiConfig.cs file trying to remember what you were aiming for, then give direct routing a try. Or, if you try direct routing but you are forever surprised by the way requests are matched because you have forgotten where you applied the attributes, then convention-based routing is worth a go.

The bottom line is that both techniques work the same behind the scenes and produce the same result: one or more routes that are used to match requests. The path you follow to generate those routes is entirely up to you, and you should take the time to experiment until you find an approach that you feel comfortable with.

Creating a Direct Route

At the heart of the direct routing feature is the Route attribute, which is defined by the RouteAttribute class in the System.Web.Http namespace. The Route attribute defines the properties shown in Table 21-3.

Table 21-3. The Properties Defined by the Route Attribute

Name

Description

Name

Specifies the name of the route. Route names are used when generating outgoing URLs.

Template

Specifies the route template that will be used to match requests. See the next section for details.

Order

Specifies the order in which routes are applied; see the “Ordering Direct Routes in a Controller” section.

There are only three properties, but as you will learn, direct routing manages to pack a lot of functionality into them, especially the routing template.

Applying the Route Attribute

To create a direct route, simply apply the Route attribute to an action method and define a route template that will match the URLs you are interested in. Multiple instances of the Route attribute can be applied to an action method, and you can apply the attribute to as many action methods as you require. Listing 21-2 shows the addition of the Route attribute to the action methods in the Today controller.

Listing 21-2. Defining Direct Routes in the TodayController.cs File

using System;
using System.Web.Http;

namespace Dispatch.Controllers {

    public class TodayController : ApiController {

        [HttpGet]
        [Route("api/today/dayofweek")]
        public string DayOfWeek() {
            return DateTime.Now.ToString("dddd");
        }

        [HttpGet]
        [Route("api/today/dayofweek/{day}")]
        public string DayOfWeek(int day) {
            return Enum.GetValues(typeof(DayOfWeek)).GetValue(day).ToString();
        }

        [HttpGet]
        [Route("getdaynumber")]
        public int DayNumber() {
            return DateTime.Now.Day;
        }
    }
}

I have applied the Route attribute to all three action methods in the controller. I have used the simplest form of the attribute, which takes a route template as its argument. The first two route templates I have defined (for the two versions of the DayOfWeek method) match the kind of URL pattern that I demonstrated in Chapter 20: there is an api prefix, followed by fixed and variable segments.

The third use of the Route attribute—on the DayNumber method—follows a different pattern, just to demonstrate that you can define route templates that match any kind of URL, even if the route template pattern is not consistent with the others defined by the controller.

Notice that I don’t have to specify the controller or action route data values. The configuration process that locates the Route attribute and sets up the direct routes uses the context in which the attribute has been applied to generate the information required for the controller and action method selection processes.

Image Note  Behind the scenes, Web API doesn’t actually set the controller and action route data values for direct routes. Direct routes use the data tokens feature, which allows data to be passed from a route to other components in the system outside of the standard route data. A data token is defined that contains a reference to the action method that the route applies to, which means the method doesn’t have to be located from the route data values. This is an optimization because the direct route system has to locate the action methods to find the Route attribute instances, and using route data would mean rendering this information to controller and action values, which would later be used to locate the action method once again. The drawback of this approach is that the meaning of the data tokens is hard-coded into the default classes that select controllers and action methods, which means you have to replicate the behavior in custom implementations.

To test the new route, start the application and use the browser to navigate to the /Home/Today URL. When you click the Get Day button, the URL requested by the client will be matched against one of the routes generated from the Route attribute, and the corresponding action method will be used to handle the request, as illustrated by Figure 21-2.

9781484200865_Fig21-02.jpg

Figure 21-2. The effect of creating direct routes

Defining a Common Prefix

The RoutePrefix attribute can be applied to a controller to define a common prefix for routes defined with the Route attribute, which can help simplify the use of the attribute. In Listing 21-3, you can see how I have added the RoutePrefix attribute to the Today controller.

Listing 21-3. Applying a Common Prefix in the TodayController.cs File

using System;
using System.Web.Http;

namespace Dispatch.Controllers {

    [RoutePrefix("api/today")]
    public class TodayController : ApiController {

        [HttpGet]
        [Route("dayofweek")]
        public string DayOfWeek() {
            return DateTime.Now.ToString("dddd");
        }

        [HttpGet]
        [Route("dayofweek/{day}")]
        public string DayOfWeek(int day) {
            return Enum.GetValues(typeof(DayOfWeek)).GetValue(day).ToString();
        }

        [HttpGet]
        [Route("~/getdaynumber")]
        public int DayNumber() {
            return DateTime.Now.Day;
        }
    }
}

I have used the RoutePrefix attribute to define a common prefix of api/today and updated the template used for the Route attributes I applied to the DayofWeek methods.

The route template that I defined for the DayNumber method doesn’t share a common prefix with the other direct routes in the controller. To prevent the prefix from being applied, I have updated the route template so that it begins with ~/, like this:

...
[Route("~/getdaynumber")]
...

Defining Optional Segments

Direct routes support optional segments directly in the route template, which provides a more natural syntax than is available in convention-based routing. In Listing 21-4, you can see how I have made the id segment optional in a route and used this to collapse together two action methods.

Listing 21-4. Defining an Optional Segment in the TodayController.cs File

using System;
using System.Web.Http;

namespace Dispatch.Controllers {

    [RoutePrefix("api/today")]
    public class TodayController : ApiController {

        //[HttpGet]
        //[Route("dayofweek")]
        //public string DayOfWeek() {
        //    return DateTime.Now.ToString("dddd");
        //}

        [HttpGet]
        [Route("dayofweek/{day?}")]
        public string DayOfWeek(int day = -1) {
            if (day != -1) {
                return Enum.GetValues(typeof(DayOfWeek)).GetValue(day).ToString();
            } else {
                return DateTime.Now.ToString("dddd");
            }
        }

        [HttpGet]
        [Route("~/getdaynumber")]
        public int DayNumber() {
            return DateTime.Now.Day;
        }
    }
}

The overall effect of an optional segment is the same in a direct route, but there are some important implementation differences. First, the segment is marked as optional by appending a ? character after the variable name so that the day segment becomes the day? segment, like this:

...
 [Route("dayofweek/{day?}")]
...

I also have to set a default value on the action method parameter, as follows:

...
public string DayOfWeek(int day = -1) {
...

The route that the Route attribute generates will match URLs with a day segment (such as /api/today/dayofweek/2) and without (such as /api/today/dayofweek). For URLs that do not contain a day segment, the route data will not contain a day value, and the default parameter value will be used instead.

Image Caution  The route won’t match URLs properly if you define an optional route template segment but forget to set a default parameter value.

In the listing, I have assigned a default parameter value of -1 to the day parameter, and I check for this value to see whether I should return today’s name or the name of a specific day of the week.

There is, however, a problem with this approach, which is that I can’t tell whether I have received a value of -1 because the client requested a URL without a day segment or because the day segment was provided, but with a value of -1. This may seem like a subtle distinction, but a URL with a day segment of -1 (meaning /api.today/dayofweek/-1) is something that I should deal with using an error since there is no corresponding day of the week. (I explain how to handle this kind of error using the model validation feature in Chapter 18.) The action method shown in the listing handles badly formed requests by ignoring the problem and pretending that a different URL has been sent, which is likely to lead to confusion. Listing 21-5 shows how I have revised the action method to take better advantage of the direct routing optional segment.

Listing 21-5. Handling an Optional Segment in the TodayController.cs File

...
[HttpGet]
[Route("dayofweek/{day?}")]
public IHttpActionResult DayOfWeek(int day = -1) {
    if (RequestContext.RouteData.Values.ContainsKey("day")) {
        return day != -1
            ? Ok(Enum.GetValues(typeof(DayOfWeek)).GetValue(day).ToString())
            : (IHttpActionResult)BadRequest("Value Out of Range");
    } else {
        return Ok(DateTime.Now.ToString("dddd"));
    }
}
...

In this implementation of the action method, I obtain the IHttpRouteData object via the RequestContext.RouteData property and check to see whether there is a day routing variable. (The RequestContext property is defined by the ApiController class, which is the base for the Today controller and which I describe in Chapter 22.)

I have changed the return type of the action method to IHttpActionResult, which allows me to send an error response when the request URL includes a day segment that is -1 and a success response otherwise.

Image Tip  I am showing you only how to differentiate between a default parameter value and a value provided by the client in this example. See Chapter 18 for details of how to validate data properly.

Defining a Default Segment Value

Direct routes also define default segment values within the route template. Listing 21-6 shows how I have changed the optional segment defined in the Today controller to one that has a default value.

Listing 21-6. Defining a Default Segment Value in the TodayController.cs File

...
[HttpGet]
[Route("dayofweek/{day=-1}")]
public string DayOfWeek(int day) {
    if (day != -1) {
        return Enum.GetValues(typeof(DayOfWeek)).GetValue(day).ToString();
    } else {
        return DateTime.Now.ToString("dddd");
    }
}
...

The default value is defined by using the equal sign after the segment name followed by the default value, expressed literally, like this:

...
[Route("dayofweek/{day=-1}")]
...

Defining a default value means that I don’t need to define a default parameter value, but it also means that I can’t tell whether the request contained a matching segment (which is the same behavior that default segment values the convention-based routes provide), so I have returned to the simpler implementation of the action method.

Image Tip  There is one important difference between a default segment value defined in a route and a default parameter value used for an optional segment: the default segment value is processed through the parameter/model binding processes that I described in Part 2. This can be useful if you are using bindings to validate data, but it also means there is no compile-time checking of the default value. Take care to test that your default values are valid, regardless of whether they are defined in direct or convention-based routes.

Applying a Constraint to a Direct Route

In addition to default and optional segments, direct route templates are also used to apply constraints. As I explained in Chapter 20, the System.Web.Http.Routing.Constraints namespace contains classes that can be used to constrain the range of URLs that a route will match. Listing 21-7 shows how I have applied one of the constraints to the day segment variable in the direct route I defined in the Today controller.

Listing 21-7. Constraining a Route in the TodayController.cs File

...
[HttpGet]
[Route("dayofweek/{day:int=-1}")]
public string DayOfWeek(int day) {
    if (day != -1) {
        return Enum.GetValues(typeof(DayOfWeek)).GetValue(day).ToString();
    } else {
        return DateTime.Now.ToString("dddd");
    }
}
...

Image Caution  As I explained in Chapter 20, constraints should be used only to manage the set of URLs that a route will match and not to validate the data that the client sends. See Chapter 18 for details of the model validation feature, which is how data should be validated.

The constraint is applied by using a colon (the : character) after the segment name, followed by a shorthand reference for the constraint that is required. Each of the constraint classes has a shorthand name, and int, which I used in the listing, applies the IntRouteConstraint class, which has the effect of matching only the URLs where the day segment can be parsed to an int value.

...
[Route("dayofweek/{day:int=-1}")]
...

I applied the constraint alongside the default value in this example, but this is not required, and default values and constraints are independent of one another. Table 21-4 lists the shorthand values and the classes they represent.

Table 21-4. The Shorthand References for Constraint Classes Used in Direct Route Templates

Short Hand

Class

Description

alpha

AlphaRouteConstraint

Matches a route when the segment variable contains only alphabetic characters

bool

BoolRouteConstraint

Matches a route when the segment variable contains only true or false

datetime

DateTimeRouteConstraint

Matches a route when the segment variable can be parsed as a DateTime object

decimal
double
float
int
long

DecimalRouteConstraint
DoubleRouteConstraint
FloatRouteConstraint
IntRouteConstraint
LongRouteConstraint

Matches a route when the segment variable can be parsed as a decimal, double, float, int, or long value

maxlength
minlength

MaxLengthRouteConstraint
MinLengthRouteConstraint

Matches a route when the segment variable is a string with a maximum or minimum length

max
min

MaxRouteConstraint
MinRouteConstraint

Matches a route when the segment variable is an int with a maximum or minimum value

range

RangeRouteConstraint

Matches a route when the segment variable is an int within a range of values

regex

RegexRouteConstraint

Matches a route when the segment variable matches a regular expression

Parameters to configure constraints are defined literally within the route template. In Listing 21-8, you can see how I have applied the range constraint to limit the range of values that the day segment will match.

Listing 21-8. Using a Direct Route Constraint with Parameters in the TodayController.cs File

using System;
using System.Web.Http;

namespace Dispatch.Controllers {

    [RoutePrefix("api/today")]
    public class TodayController : ApiController {

        [HttpGet]
        [Route("dayofweek")]
        public string DayOfWeek() {
            return DateTime.Now.ToString("dddd");
        }

        [HttpGet]
        [Route("dayofweek/{day:range(0, 6)}")]
        public string DayOfWeek(int day) {
            return Enum.GetValues(typeof(DayOfWeek)).GetValue(day).ToString();
        }

        [HttpGet]
        [Route("~/getdaynumber")]
        public int DayNumber() {
            return DateTime.Now.Day;
        }
    }
}

I have used the range shorthand to apply the RangeRouteConstraint class to the day segment, and the parameters I have specified will allow the route to match a URL if the segment value is an int between 0 and 6. Constraining the route means I can’t use an optional segment (the two are counter functional), so I have restored the version of the DayOfWeek method that takes no parameters.

Ordering Direct Routes in a Controller

As I explained in Chapter 20, the URL routing system enumerates the routes in the application until it finds one that can match the current request. No effort is made to find the best match—just the first one, after which all of the untested routes are ignored. When using convention-based routing, the order in which the routes are added to the HttpRouteCollection class is used to specify the order in which routes are tested against requests.

For direct routing, the routes defined by the Route attribute are automatically sorted so that the most specific routes are registered first, irrespective of the order in which the action methods are defined in the controller class.

To work out the order in which direct routes in a controller are applied, the URL routing feature calculates the precedence of each segment in the route template of each direct route. The precedence is a decimal value, which is then used to sort the routes so that the lowest values match first. For each segment, a score is awarded based on the segment type, as described in Table 21-5.

Table 21-5. The Scores Assigned to Direct Route Segment Types

Segment Type

Score

Fixed segment

1

Variable segment with a constraint

2

Variable segment without a constraint

3

Catchall segment with a constraint

4

Catchall segment without a constraint

5

The scores are concatenated (not summed) to form a single decimal value. To explain how this works, Table 21-6 shows the segments from one of the direct routes in the Today controller, along with the segment types and scores. (Notice that the segments defined by the RoutePrefix attribute are included.)

Table 21-6. The Scores for an Example Direct Route

Segment

Segment Type

Score

api

Fixed segment

1

today

Fixed segment

1

dayofweek

Fixed segment

1

day:range(0, 6)

Variable segment with a constraint

2

The individual scores are concatenated to form the precedence value 1.112 (the first score is always expressed as a whole number and subsequent scores as decimal fractions). Table 21-7 shows all of the routes defined in the Today controller and their precedence values.

Table 21-7. The Precedence Values for the Direct Routes in the Today Controller

Route

Precedence

api/today/dayofweek

1.11

api/today/dayofweek/{day:range(0, 6)}

1.112

getdaynumber

1.0

The lowest-precedence routes are used to match requests first, which produces the following route order:

  1. /getdaynumber (precedence 1.0)
  2. /api/today/dayofweek (precedence 1.11)
  3. /api/today/dayofweek/{day} (precedence 1.112)

The precedence system usually creates a useful ordering of routes, but you can get odd results if a controller defines two routes that have the same precedence because Web API compares the route templates as alphabetic strings.

Using the alphabet to resolve route ordering isn’t especially helpful, but you can use the Order property defined by the Route attribute to take control of the order in which routes are checked, as demonstrated in Listing 21-9.

Listing 21-9. Applying the Order Property to the Route Attribute in the TodayController.cs File

using System;
using System.Web.Http;

namespace Dispatch.Controllers {

    [RoutePrefix("api/today")]
    public class TodayController : ApiController {

        [HttpGet]
        [Route("dayofweek")]
        public string DayOfWeek() {
            return DateTime.Now.ToString("dddd");
        }

        [HttpGet]
        [Route("dayofweek/{day:range(0, 6)}")]
        public string DayOfWeek(int day) {
            return Enum.GetValues(typeof(DayOfWeek)).GetValue(day).ToString();
        }

        [HttpGet]
        [Route("~/getdaynumber", Order=1)]
        public int DayNumber() {
            return DateTime.Now.Day;
        }
    }
}

Routes are assigned an Order value of 0 by default, and the routes with the lowest Order values are checked first. By setting the Order property to 1, I have demoted the route defined for the DayNumber method, producing the following route order:

  1. /api/today/dayofweek (order 0, precedence 1.11)
  2. /api/today/dayofweek/{day} (order 0, precedence 1.112)
  3. /getdaynumber (order 1, precedence 1.0)

Image Note  The Order value is checked first, but if there are routes with the same Order value, then the precedence score is taken into account.

Creating a Controller-wide Direct Route

In the previous section, I applied the Route attribute to individual action methods to create direct routes, but you can also apply the attribute to the controller class to create a direct route that applies to any action method for which a direct route has not already been defined. Listing 21-10 shows how I applied the Route attribute to the Today controller.

Listing 21-10. Applying the Route Attribute to the Controller in the TodayController.cs File

using System;
using System.Web.Http;

namespace Dispatch.Controllers {

    [RoutePrefix("api/today")]
    [Route("{action=DayOfWeek}")]
    public class TodayController : ApiController {

        [HttpGet]
        //[Route("dayofweek")]
        public string DayOfWeek() {
            return DateTime.Now.ToString("dddd");
        }

        [HttpGet]
        [Route("dayofweek/{day:range(0, 6)}")]
        public string DayOfWeek(int day) {
            return Enum.GetValues(typeof(DayOfWeek)).GetValue(day).ToString();
        }

        [HttpGet]
        //[Route("~/getdaynumber", Order=1)]
        public int DayNumber() {
            return DateTime.Now.Day;
        }
    }
}

I have applied the Route attribute to the class with a template of {action=DayOfWeek}, which is combined with the RoutePrefix template to create this template:

api/today/{action=DayOfWeek}

By providing a default value for the action variable, I have created a route that will match URLs that specify action methods (such as /api/today/daynumber and /api/today/dayofweek). The route template will also match a URL that doesn’t specify an action method (/api/today) and will use the DayOfWeek method by default.

I have left the Route attribute applied to the DayOfWeek method that takes a parameter, which means that it will not be covered by the controller-wide Route attribute. However, if I apply the Route attribute to a controller, I generally prefer to define all the routes at that level because I end up forgetting that there are method-specific routes defined as well. Listing 21-11 shows how I consolidated all of the direct routes in the Today controller.

Listing 21-11. Consolidating the Direct Routes in the TodayController.cs File

using System;
using System.Web.Http;

namespace Dispatch.Controllers {

    [RoutePrefix("api/today")]
    [Route("{action=DayOfWeek}")]
    [Route("{action=DayOfWeek}/{day:range(0, 6)}")]
    public class TodayController : ApiController {

        [HttpGet]
        public string DayOfWeek() {
            return DateTime.Now.ToString("dddd");
        }

        [HttpGet]
        public string DayOfWeek(int day) {
            return Enum.GetValues(typeof(DayOfWeek)).GetValue(day).ToString();
        }

        [HttpGet]
        public int DayNumber() {
            return DateTime.Now.Day;
        }
    }
}

Image Tip  This is my personal practice, and I suspect it arises because I am trying to re-create the centralization of convention-based routing while using the Route attribute. You need not adopt this convention if you are comfortable with defining direct routes throughout the application.

Customizing URL Routing

As you have learned, the default behavior of the routing system provides a lot of flexibility to manage the API that web services present to their clients. That said, if you find you are unable to create the behavior that you want, there are several ways in which you can customize the routing process, as I describe in the following section.

Using a Route-Specific Message Handler

If you are using convention-based routing, you can specify a message handler that will be used to process a request when it is matched by the route, allowing a request to be dealt with outside of the standard dispatch handler chain. (This feature is not available for direct routing.) Listing 21-12 shows the contents of the CustomRouteHandler.cs file, which I added to the Infrastructure folder.

Listing 21-12. The Contents of the CustomRouteHandler.cs File

using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace Dispatch.Infrastructure {
    public class CustomRouteHandler : HttpMessageHandler {

        protected override Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage request, CancellationToken cancellationToken) {

            return Task.FromResult<HttpResponseMessage>(
                request.CreateResponse(HttpStatusCode.OK, "Today"));
        }
    }
}

Image Tip  I find route-specific message handlers useful when I need to support a legacy API that doesn’t quite fit into the Web API model, where it can be useful to redirect the client to other URLs or return fixed responses for certain requests. Otherwise, I use technique sparingly because it changes the normal flow of requests through the application and creates a special category of requests that will need to be tested thoroughly for every new release.

When I showed you how to add a message handler to the dispatch chain in Chapter 19, I derived my custom class from the DelegatingHandler class so that Web API could provide a reference to the next handler in the chain.

There is no chain when you set a handler for a route, so I have derived the CustomRouteHandler class directly from HttpMessageHandler. I have implemented the SendAsync method so that I create and return an HttpResponseMessage with the 200 (OK) status code and the string Today as the result.

Registering the Route and Handler

When defining a route, a custom handler can be specified using a version of the MapHttpRoute extension method, as shown in Listing 21-13.

Listing 21-13. Creating a Route with a Custom Handler in the WebApiConfig.cs File

using System.Web.Http;
using Dispatch.Infrastructure;

namespace Dispatch {
    public static class WebApiConfig {
        public static void Register(HttpConfiguration config) {

            config.Routes.MapHttpRoute(
                name: "CustomHandler",
                routeTemplate: "api/{controller}/{action}",
                defaults: null,
                constraints: null,
                handler: new CustomRouteHandler());

            config.MapHttpAttributeRoutes();

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

I have defined a routing template that will match all three-segment URLs that start with /api. The null values tell the routing system that I don’t want to use default values or constraints. The final argument is the handler that should be used to process the request when the route matches, which is an instance of the CustomRouteHandler class that I defined in the previous section.

To test the custom handler, start the application and use the browser to navigate to the /Home/Today URL. When you click the Get Day button, the client will send an Ajax request to the /api/today/dayofweek URL. The URL will be matched by the new route, and the CustomRouteHandler class will be used to send the Today response to the client, as illustrated by Figure 21-3.

9781484200865_Fig21-03.jpg

Figure 21-3. Using a custom message handler in a route

Using Data Tokens

Routes can be defined with data tokens, which are expressed as a Dictionary<string, object> and are used to provide additional information to objects that will process the request. I am not a fan of using data tokens—for reasons I explain in the sidebar “The Problem with Data Tokens”—and I recommend you approach them with caution.

Listing 21-14 shows how I have redefined the route with a custom message handler in the WebApiConfig.cs file so that it defines data tokens. There is no version of the MapHttpRoute extension method that allows data tokens to be specified, so I have to use the CreateRoute and Add methods defined by the HttpRouteCollection object, as described in Chapter 20.

Listing 21-14. Defining a Route with Data Tokens in the WebApiConfig.cs File

using System.Web.Http;
using Dispatch.Infrastructure;
using System.Collections.Generic;

namespace Dispatch {
    public static class WebApiConfig {
        public static void Register(HttpConfiguration config) {

            config.Routes.Add(
                "CustomHandler",
                config.Routes.CreateRoute(
                    routeTemplate: "api/{controller}/{action}",
                    defaults: null,
                    constraints: null,
                    dataTokens: new Dictionary<string, object> {
                        { "response", "Tomorrow" }
                    },
                    handler: new CustomRouteHandler()));

            config.MapHttpAttributeRoutes();

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

The Dictionary that I used to set the dataToken parameter of the CreateRoute method contains a single key, response. Listing 21-15 shows how I use this key in the CustomRouteHandler class to set the data in the response to the client.

Listing 21-15. Consuming Data Tokens in the CustomRouteHandler.cs File

using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace Dispatch.Infrastructure {
    public class CustomRouteHandler : HttpMessageHandler {

        protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage
            request, CancellationToken cancellationToken) {

            string responseString
                = (string)request.GetRequestContext()
                    .RouteData.Route.DataTokens["response"];

            return Task.FromResult<HttpResponseMessage>(
                request.CreateResponse(HttpStatusCode.OK, responseString));
        }
    }
}

Data tokens are defined for the route, rather than for each request that the route matches, and they are accessed through the DataTokens property defined by the IHttpRoute interface, which I described in Chapter 20. To get the IHttpRoute implementation object that matched the request, I call the GetRequestContext extension method on the HttpRequestMessage object to get an instance of the HttpRequestContext class and then read the Route property.

THE PROBLEM WITH DATA TOKENS

The built-in handlers and routing classes use data tokens to communicate with one another. One example is precedence information for direct routes, which I described earlier in this chapter. The problem with data tokens is that they creating a coupling between a route handler and other components, generally, the classes that select the controller and action method that will handle the request. This coupling makes it harder to create custom implementations of Web API interfaces without understanding the purpose and meaning of the data tokens, which are undocumented. In the case of direct route precedence, you either need to spend some time with the debugger and the source code to figure out how they work, which is what I did for this chapter, or re-create the feature from scratch, which requires more work and testing. My advice is to avoid data tokens in your own code and check for their use carefully when you are creating a custom implementation of a Web API interface.

You can see the effect of the data token by starting the application and navigating to the /Home/Today URL with the browser. When you click the Get Day button, the client will send a request that will be matched by the route defined in Listing 21-14, and the custom handler will read the token value to produce the result, as illustrated by Figure 21-4.

9781484200865_Fig21-04.jpg

Figure 21-4. Using a data token

Applying Custom Constraints to Direct Routes

In Chapter 20, I showed you how to apply a custom constraint to a convention-based route, but direct routes apply constraints in the route template, which is a problem because there are no built-in shorthand names for custom classes.

Fortunately, it is a simple matter to define a new shorthand name. To demonstrate how this works, I have defined a new constraint by adding the SpecificValueConstraint.cs file to the Infrastructure folder and using it to define the class shown in Listing 21-16.

Listing 21-16. The Contents of the SpecificValueConstraint.cs File

using System.Collections.Generic;
using System.Net.Http;
using System.Web.Http.Routing;

namespace Dispatch.Infrastructure {

    public class SpecificValueConstraint : IHttpRouteConstraint {
        private int targetValue;

        public SpecificValueConstraint(int value) {
            targetValue = value;
        }

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

            int candidateValue;

            return (values.ContainsKey(parameterName))
                && int.TryParse(values[parameterName].ToString(), out candidateValue)
                && targetValue == candidateValue;
        }
    }
}

This constraint checks that a segment variable is a specified int value and will prevent the route from matching the request unless it is.

Registering and Using the Constraint Shorthand Name

In Listing 21-17, you can see how I have created a shorthand name for the constraint in the WebApiConfig.cs file.

Listing 21-17. Registering a Shorthand Constraint Name in the WebApiConfig.cs File

using System.Web.Http;
using Dispatch.Infrastructure;
using System.Collections.Generic;
using System.Web.Http.Routing;

namespace Dispatch {
    public static class WebApiConfig {
        public static void Register(HttpConfiguration config) {

            //config.Routes.Add(
            //    "CustomHandler",
            //    config.Routes.CreateRoute(
            //        routeTemplate: "api/{controller}/{action}",
            //        defaults: null,
            //        constraints: null,
            //        dataTokens: new Dictionary<string, object> {
            //            { "response", "Tomorrow" }
            //        },
            //        handler: new CustomRouteHandler()));

            DefaultInlineConstraintResolver resolver
                = new DefaultInlineConstraintResolver();
            resolver.ConstraintMap.Add("specval", typeof(SpecificValueConstraint));
            config.MapHttpAttributeRoutes(resolver);

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

Image Tip  Notice that I have commented out the convention-based route that uses a custom handler so that it doesn’t preempt the direct routes defined on the Today controller.

The DefaultInlineConstraintResolver class is used to resolve shorthand constrain names and defines a property called ConstraintMap that returns a dictionary used to map shorthand names to constraint types.

In the listing, I create a new instance of the DefaultInlineConstraintResolver class and use the ConstraintMap.Add method to define a new shorthand name, specval, to represent the SpecificValueConstraint class. I then call the MapHttpAttributeRoutes method to set up the direct routes, passing in the DefaultInlineConstraintResolver object as the argument. The final step is to apply the constraint to a direct route using the shorthand name I defined in Listing 21-17. In Listing 21-18, you can see how I update the direct routes in the Today class.

Listing 21-18. Using a Custom Constraint in the TodayController.cs File

using System;
using System.Web.Http;

namespace Dispatch.Controllers {

    [RoutePrefix("api/today")]
    [Route("{action=DayOfWeek}")]
    [Route("{action=DayOfWeek}/{day:specval(2)}")]
    public class TodayController : ApiController {

        // ...action methods omitted for brevity...
    }
}

With this change, the custom constraint will prevent the highlighted route from matching unless the day segment variable is 2.

Applying a Route-wide Custom Constraint

The custom constraint in the previous section operates on a single variable segment, which fits into the direct route model of applying constraints within the route template. It doesn’t work, however, for constraints that are not specific to a segment, such as the UserAgentConstraint class that I defined in Chapter 20. This constraint applies to the entire route, which means I can’t use it in the route template.

The Web API URL system includes the abstract RouteFactoryAttribute class, which can be used to create routes that don’t fit into the standard direct routing system. The RouteFactoryAttribute class defines the virtual properties shown in Table 21-8, which can be overridden in derived classes.

Table 21-8. The Virtual Properties Defined by the RouteFactoryAttribute Class

Name

Description

Constraints

Returns the set of constraints applied to the route

DataTokens

Returns the data token for the route

Order

Returns the Order value that will be used to sort the routes

To apply the UserAgentConstraint to a direct route, I added a class file called UserAgentConstraintRouteAttribute.cs to the Infrastructure folder and used it to define the class shown in Listing 21-19.

Listing 21-19. The Contents of the UserAgentConstraintRouteAttribute.cs File

using System.Collections.Generic;
using System.Web.Http.Routing;

namespace Dispatch.Infrastructure {
    public class UserAgentConstraintRouteAttribute : RouteFactoryAttribute {

        public UserAgentConstraintRouteAttribute(string template)
            : base(template) {
        }

        public override IDictionary<string, object> Constraints {
            get {
                IDictionary<string, object> constraints
                    = base.Constraints ?? new Dictionary<string, object>();
                constraints.Add("useragent", new UserAgentConstraint("Chrome"));
                return constraints;
            }
        }
    }
}

The UserAgentConstraintRouteAttribute class derives from RouteFactoryAttribute and overrides the Constraints property to return the set of constraints defined by the base class (or creates a new dictionary if required). I added a new instance of the UserAgentConstraint class to the collection, like this:

...
constraints.Add("useragent", new UserAgentConstraint("Chrome"));
...

You can use any key to register the constraint object as long as it doesn’t correspond to a variable segment name in the route template. Listing 21-20 shows how I replaced the Route attribute with UserAgentConstraintRoute in the Today controller.

Listing 21-20. Using a Custom Route Attribute in the TodayController.cs File

using System;
using System.Web.Http;
using Dispatch.Infrastructure;

namespace Dispatch.Controllers {

    [RoutePrefix("api/today")]
    [Route("{action=DayOfWeek}")]
    [UserAgentConstraintRoute("{action=DayOfWeek}/{day:specval(2)}")]
    public class TodayController : ApiController {

        // ...action methods omitted for brevity...
    }
}

The effect is to apply the UserAgentConstraint to the route, in addition to the per-segment constraints defined in the route template.

Summary

In this chapter, I described how Web API direct routes works, allowing you to define routes on action methods or controllers, rather than in the WebApiConfig.cs file. I explained how the Route attribute is used to create direct routes, how to define optional segments, how to define segments with default values, and how constraints can be applied. I finished this chapter by showing you how to customize the routing process. In the next chapter, I continue describing the dispatch process and explain how controllers and action methods are used to handle requests.

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

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