CHAPTER 20

image

URL Routing: Part I

In Chapter 19, I explained that URL routing is integrated into the Web API dispatch process by one of the built-in message handlers. In this chapter, I explain how URL routing works in a Web API application and demonstrate how to create convention-based routes, where routes are defined in a single place and used to match requests to controllers and actions throughout the application, known as convention-based routing. In Chapter 21, I continue on the topic of URL routing and show you a different approach that defines routes through attributes applied to controller classes and action methods, known as direct or attribute-based routing. Table 20-1 summarizes this chapter.

Table 20-1. Chapter Summary

Problem

Solution

Listing

Specify the HTTP verb that an action method can receive.

Apply the one of the verb attributes, such as HttpGet.

1–7

Obtain the action method from the request URL.

Define a variable segment called action in the route template.

8

Restrict the URLs that a route will match.

Increase the use of fixed segments or apply constraints.

9, 13–15

Broaden the URLs that a route will match.

Use default segment values and optional segments.

10–12

Preparing the Example Project

I am going to continue working with the Dispatch project I created in Chapter 19, but there are some changes I need to make. Listing 20-1 shows the WebApiConfig.cs file after I deleted the statements that changed the controller class suffix and the statement that registers the message handler that causes the debugger to break for POST requests.

Listing 20-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 }
            );
        }
    }
}

I also need to add a new Web API controller so that I can define routes that target it later in the chapter. Listing 20-2 shows the contents of the TodayController.cs file that I added to the Controllers folder.

Listing 20-2. The Content of the TodayController.cs File

using System;
using System.Web.Http;

namespace Dispatch.Controllers {

    public class TodayController : ApiController {

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

This is a controller with a single action method that returns the name of the current day as a string. Getting the current day doesn’t make for an interesting web service, but it is enough functionality for me to demonstrate how the Web API URL routing system works.

I need to create a client that will target the controller, and in Listing 20-3 you can see the action method I added to the HomeController.cs file.

Listing 20-3. Adding an Action in the HomeController.cs File

using System.Web.Mvc;

namespace Dispatch.Controllers {

    public class HomeController : Controller {

        public ActionResult Index() {
            return View();
        }

        public ActionResult Today() {
            return View();
        }
    }
}

The new action renders the default view associated. You can see that view in Listing 20-4, which shows the content of the Today.cshtml file I created in the /Views/Home folder.

Listing 20-4. The Contents of the Today.cshtml file

@{ Layout = null;}
<!DOCTYPE html>
<html>
<head>
    <meta name="viewport" content="width=device-width" />
    <title>Today</title>
    <link href="~/Content/bootstrap.min.css" rel="stylesheet" />
    <link href="~/Content/bootstrap-theme.min.css" rel="stylesheet" />
    <script src="~/Scripts/jquery-2.1.0.min.js"></script>
    <script src="~/Scripts/knockout-3.1.0.js"></script>
    <script src="~/Scripts/today.js"></script>
    <style>
        body { padding-top: 10px; }
    </style>
</head>
<body class="container">
    <div class="alert alert-success" data-bind="css: { 'alert-danger': gotError }">
        <span data-bind="text: response()"></span>
    </div>
    <button class="btn btn-primary" data-bind="click: sendRequest">Get Day</button>
</body>
</html>

This is the same basic approach I have been using for many of the MVC views in this book. There is a Bootstrap-styled div element that provides information about the outcome of the Ajax requests that the client makes and a button element that uses Knockout to invoke a JavaScript function. The JavaScript function that the button invokes is called sendRequest, and I defined it in the today.js file, which I added to the Scripts folder and which is shown in Listing 20-5.

Listing 20-5. The Contents of the today.js File

var response = ko.observable("Ready");
var gotError = ko.observable(false);

var sendRequest = function () {
    $.ajax("/api/today/dayofweek", {
        type: "GET",
        success: function (data) {
            gotError(false);
            response(data);
        },
        error: function (jqXHR) {
            gotError(true);
            response(jqXHR.status + " (" + jqXHR.statusText + ")");
        }
    });
};

$(document).ready(function () {
    ko.applyBindings();
});

Testing the Application Changes

To test the changes for this chapter, start the application and use the browser to navigate to the /Home/Today URL. This will cause the MVC framework to render the Today.cshtml view for the browser. Click the Get Day button, and you will see a 405 (Method Not Allowed) error message displayed in the alert div element at the top of the window, as illustrated by Figure 20-1.

9781484200865_Fig20-01.jpg

Figure 20-1. Testing the new controller

The default routing configuration of the application doesn’t match the Ajax request sent by the client to the DayOfWeek action method defined by the new Today web service controller. I explain why this is—and how it can be resolved—in the sections that follow.

Understanding URL Routing

The purpose of URL routing is to match HTTP requests to routes, which contain instructions for producing routing data that is consumed by other components.

The HttpRoutingDispatcher message handler is responsible for processing HttpRequestMessage objects in order to produce routing data and assign it to the HttpRequestContext.RouteData property. Figure 20-2 shows the dispatch process with the details I described in Chapter 19.

9781484200865_Fig20-02.jpg

Figure 20-2. The Web API dispatch process

This chapter is all about the URL routing part of the diagram, which represents the process by which the routing system processes requests so that a URL such as this will produce the routing data values products and 1 for the controller and id properties.

/api/products/1

Table 20-2 puts URL routing in context.

Table 20-2. Putting URL Routing in Context

Question

Answer

What is it?

URL routing processes requests in order to extract data that is used by other components in the dispatch process, such as the classes that select controllers and action methods.

When should you use it?

URL routing is applied to all requests automatically.

What do you need to know?

The default route defined in the WebApiConfig.cs file is suitable for simple RESTful web services, but most complex applications will need some form of customization.

Image Note  Remember that the URL routing system just generates routing data; it doesn’t use that data to modify the request (other than to set the HttpRequest.RouteData property) or generate the response. Those tasks are handled by the HttpControllerDispatcher and its interfaces, which I described in Chapter 21 and return to again in Chapter 21.

Understanding the Routing Classes and Interfaces

There are four important types within URL routing: the IHttpRoute and IHttpRouteData interfaces and the HttpRouteCollection class. The IHttpRoute interface describes a route, and Web API provides a default implementation—the HttpRoute class—that is used in most applications. In the sections that follow, I describe each of these types and the members they define, and I’ll show you how they work together throughout this chapter and in Chapter 21. For quick reference, I have summarized the important types in Table 20-3.

Table 20-3. The Most Important URL Routing Classes and Interfaces

Name

Description

IHttpRoute

This interface describes a route. See the “Understanding the IHttpRoute Interface” section.

HttpRoute

This is the default implementation of the IHttpRoute interface.

IHttpRouteData

This interface describes the collection of data values extracted from a request. See the “Understanding the IHttpRouteData Interface” section.

HttpRouteData

This is the default implementation of the IHttpRouteData interface.

IHttpRouteConstraint

This interface defines a restriction that limits the requests that a route will match. See the “Using Routing Constraints” section.

HttpRouteCollection

This is the class with which routes are registered and which receives requests from the HttpRoutingDispatcher. See the “Understanding the HttpRouteCollection Class” section.

HttpRoutingDispatcher

This message handler class integrates routing into the dispatch process. See Chapter 19.

RouteAttribute

This class defines the Route attribute used to create direct routes on controller classes and action methods. See Chapter 21.

RouteFactoryAttribute

This class allows custom attributes to be defined that customize the generation of direct routes. See Chapter 21.

RoutePrefix

This attribute is used to define a route template prefix that applies to all of the direct routes defined on a controller. See Chapter 21.

Using the classes I have described in the table, I can update my diagram of the dispatch process, as shown in Figure 20-3. I explain how the different interfaces and classes operate in the sections that follow and in Chapter 19.

9781484200865_Fig20-03.jpg

Figure 20-3. Updating the dispatch diagram

Understanding the IHttpRouteData Interface

The IHttpRouteData interface describes the collection of data values that are extracted from a request when it is processed. The interface defines the properties shown in Table 20-4.

Table 20-4. The Properties Defined by the IHttpRouteData Interface

Name

Description

Route

Returns the IHttpRoute object that generated the route data

Values

Returns an IDictionary<string, object> that contains the routing data

An implementation of the IHttpRouteData interface is the result of the URL routing process and the means by which the routing system provides data about the request for other components to consume.

The Values property is used to access the routing data that has been extracted from a request. Most routing data is expressed as string values, but routes can produce any kind of data that other components may find helpful, which is why the Values property returns a dictionary that maps keys to objects, rather than just strings. The default implementation of the IHttpRouteData interface is the HttpRouteData class.

Understanding the IHttpRoute Interface

Routes are described by the IHttpRoute interface, which defines the properties and methods listed in Table 20-5. You generally work with route objects indirectly, as I explain in the next section, but the properties and methods defined by the IHttpRoute interface are useful in understanding how URL routing works, and you will see how the types they return fit together throughout this chapter.

Table 20-5. The Methods and Properties Defined by the IHttpRoute Interface

Name

Description

RouteTemplate

Returns the template used to match requests. See the “Using Route Templates” section.

Defaults

Returns an IDictionary<string, object> used to provide default values for routing data properties when they are not included in the request. Defaults are usually defined as a dynamic object, as demonstrated in the “Using Routing Data Default Values” section.

Constraints

Returns an IDictionary<string, object> used to restrict the range of requests that the route will match. Constraints are usually defined as a dynamic object, as demonstrated in the “Using Routing Constraints” section.

DataTokens

Returns an IDictionary<string, object> with data values that are available to the routing handler. See Chapter 21.

Handler

Returns the HttpMessageHandler onto which the request will be passed. This property overrides the standard dispatch process.

GetRouteData(path, request)

Called by the routing system to generate the routing data for the request.

Understanding the HttpRouteCollection Class

The HttpRouteCollection class orchestrates the entire routing process, and as a consequence, it plays several different roles.

First, the HttpRouteCollection provides the CreateRoute method that creates new routes using the HttpRoute class, which is the default implementation of the IHttpRoute interface. There are several versions of the CreateRoute method, as described in Table 20-6. This is the convention-based style of routing that I described in Chapter 19 and is used to define routes in the WebApiConfig.cs file.

Table 20-6. The HttpRouteCollection Methods for Creating New Routes

Name

Description

CreateRoute(template,defaults, constraints)

Returns an IHttpRoute implementation object that has been configured with the specified template, defaults, and constraints

CreateRoute(template, defaults,constraints, tokens)

Returns an IHttpRoute implementation object that has been configured with the specified template, defaults, constraints, and tokens

CreateRoute(template, defaults,constraints, tokens, handler)

Returns an IHttpRoute implementation object that has been configured with the specified template, defaults, constraints, tokens, and message handler

The different versions of the CreateRoute method all take parameters that correspond directly to the properties defined by the IHttpRoute method. Using the CreateRoute method allows you to obtain implementations of the IHttpRoute interface without tightly coupling your code to a specific implementation class, although there is nothing to stop you from creating your own implementation of the interface or simply instantiating the HttpRoute class directly.

The CreateRoute method creates the route, but it doesn’t register it so that it will be used to match requests. The second role that the HttpRouteCollection class plays is to provide a collection that is used to register routes for use with an application. Table 20-7 lists the methods that provide the collection feature.

Table 20-7. The Collection Members Defined by the HttpRouteCollection Class

Name

Description

Count

This returns the number of routes in the collection.

Add(name, route)

This adds a new route to the collection.

Clear()

This removes all the routes from the collection.

Contains(route)

This returns true if the collection contains the specified route.

ContainsKey(name)

This returns true if the collection contains a route with the specified name.

Insert(index, name, route)

This inserts a route with the specified name at the specified index.

Remove(name)

This removes the route with the specified name from the collection.

TryGetValue(name, out route)

This attempts to retrieve a route with the specified name from the collection. If there is a route with that name, the method returns true and assigns the route to the out parameter.

this[int]

The HttpRouteCollection class defines an array-style indexer that retrieves routes by their position in the collection.

this[name]

The HttpRouteCollection class defines an array-style indexer that retrieves routes by their name.

Image Note  As you will learn, routes are tested to see whether they can match a request, which means that the order in which the routes are added to the collection is important. Just as with the MVC framework, you should add the most specific routes first so that they are able to match requests before more general routes.

If you use the HttpRouteCollection class methods, then setting up a new route requires two steps: a call to the CreateRoute method to create a new IHttpRoute object and a call to the Add or Insert method to add the route to the collection.

A more common approach is to use the extension methods that are defined on the HttpRouteCollection class, which allow routes to be set up in a single step. Table 20-8 shows the available extension methods.

Table 20-8. The HttpRouteCollection Extension Methods

Name

Description

IgnoreRoute(name, template)

Creates and registers a route with the specified name and template that prevents a request from being handled by Web API

IgnoreRoute(name, template,constraints)

Creates and registers a route with the specified name, template, and constraints that prevents a request from being handled by Web API

MapHttpBatchRoute(name,template handler)

Creates and registers a route for the batch handling of HTTP requests

MapHttpRoute(name, template)

Creates and registers a route with the specified name and template

MapHttpRoute(name, template,defaults)

Creates and registers a route with the specified name, template, and defaults

MapHttpRoute(name, template,defaults, constraints)

Creates and registers a route with the specified name, template, defaults, and constraints

MapHttpRoute(name, template,defaults, constraints, handler)

Creates and registers a route with the specified name, template, defaults, constraints, and message handler

Understanding the Route Attributes

The Route attribute—defined as the RouteAttribute class in the System.Web.Http.Routing namespace—is applied directly to classes and methods. This is the direct or attribute style of routing, where the routes are more specific than those in the WebConfig.cs file and are defined alongside the code that will handle the request.

Image Note  Microsoft hasn’t settled on clear terminology for routes that are created using attributes applied to controller classes or action methods. They switch between the terms attribute-based routes and direct routes, with the latter term being emphasized in the names of classes and interfaces in the System.Web.Http.Routing namespace. It doesn’t matter which term you use, but I have tried to be consistent in this chapter and use direct routes.

Working with Convention-Based Routing

In this section, I am going to use the WebApiConfig.cs file to define a series of convention-based routes that will demonstrate the different ways in which you can match requests and generate routing data. Many of the techniques apply equally to direct routes, which I describe in Chapter 21. Table 20-9 puts convention-based routing in context.

Table 20-9. Putting Convention-Based Routing in Context

Question

Answer

What is it?

Convention-based routing defines URL routes in a single location—the WebApiConfig.cs file—for the entire application. The alternative is to define routes by applying attributes to classes and methods, which I describe in Chapter 19.

When should you use it?

The choice between convention-based routing and defining routes with attributes is largely a matter of personal preference, as I explain in Chapter 21.

What do you need to know?

The default routing configuration relies on matching action methods based on the HTTP verb. Define a custom route with an action variable segment if you want to specify an action method in the URL. See the “Routing to the New Controller” section for details.

Using Route Templates

Templates are at the heart of the routing system and are the start point for matching requests and extracting information from the URL. Web API route templates work in the same way as those in the MVC framework, and you can see an example of a Web API route template in the WebApiConfig.cs file, where Visual Studio has set up the default convention-based route for web services.

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 }
            );
        }
    }
}

The routing system treats URLs as a series of segments separated by the / character. The URL http://myhost.com/api/products/1 has three segments: api, products, and 1. (The protocol, port, and hostname are all ignored.)

Image Tip  The terminology here gets a bit muddled because there are segments in the request URL and in the routing template. Don’t worry if it doesn’t make immediate sense—it will all start to fall into place through examples.

Routing templates match requests based on the segments in the URL that has been asked for using a system of fixed (or static) and variable segments. Fixed segments will match URLs only if they have the same text in the corresponding segment. As an example, the routing template for the default route has one fixed segment: api. This means the route will match only URLs whose first segment is the string api. URLs that have different first segments will not be matched by the route template.

Image Tip  Route templates are defined without a leading / character. If you do put in a leading /, then an exception will be thrown when the application is started.

The template variable segments will match any URL that has a corresponding segment, irrespective of what the value of the segment is. Variable segments are denoted with the { and } characters, and the value of the URL segment is assigned to a variable of the specified name in the routing data, known—confusingly—as segment variables.

The default route template contains two variable segments, as follows:

...
routeTemplate: "api/{controller}/{id}",
...

The template will match any URL that contains three segments where the first segment is api. The contents of the second and third segments will be assigned to route data variables called controller and id.

Image Tip  You can vary the set of URLs that a route template will match by using constraints and defaults, which I explain in the “Controlling Route Matching” section.

Routing to the New Controller

Two segment variables have special importance in Web API: controller and action. The controller variable is used to match the controller that will be used to handle the request, as I explained in Chapter 19. The action variable can be used to specify the action method defined by the controller, just as in the MVC framework, but there isn’t a segment to capture this variable in the default route.

This is because Web API uses the HTTP verb from the request to select an action method by default. I explain the action method selection process in detail in Chapter 22, but as part of the drive toward RESTful web services, Web API takes notice of the type of HTTP request.

The reason that the client code that I added at the start of the chapter can’t reach the new controller is because the action method it contains doesn’t provide the selection process with the information it requires to perform the default action method selection.

The URL that the client requests is as follows:

/api/today/dayofweek

The api prefix matches the fixed segment at the start of the route template. The variable segments extract a value of today for the controller variable and dayofweek for the id property. This doesn’t provide the selection mechanism with enough information to match the request to an action method, which is why an error is reported. There are two ways to get Web API to route requests to the new controller.

Mapping Request Verbs to Action Methods

One way to give the action method selection mechanism the information it requires is to specify which HTTP verbs an action method can handle. For my example controller, I need to specify that the DayOfWeek action method should be used for GET requests, which is the request type that the jQuery client is sending. You can see how I did this in Listing 20-6.

Listing 20-6. Associating an HTTP Verb with an Action Method in the TodayController.cs File

using System;
using System.Web.Http;

namespace Dispatch.Controllers {

    public class TodayController : ApiController {

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

The HttpGet attribute is one of a set that Web API provides so that you can specify which HTTP methods an action method can receive. There are attributes for different HTTP verbs: HttpGet, HttpPost, HttpPut, and so on.

You don’t need to use these attributes if the action methods in your controller follow the Web API RESTful pattern, which is why I have not had to apply attributes to the action methods in the Products controller. I explain the pattern that Web API looks for to match verbs to action methods in Chapter 22, but for this chapter it is enough to know that you can provide the verb information needed to select the action method using one of the verb attributes.

However, caution is required because it is easy to create an unwanted effect. Using a verb attribute allows the default route to direct requests to the DayOfWeek action method, but it does so using only part of the URL that has been requested. As a reminder, here is the default route template:

...
routeTemplate: "api/{controller}/{id}"
...

And here is the URL from the today.js file that jQuery uses to make the HTTP request:

...
$.ajax("/api/today/dayofweek", {
...

The problem is that the part of the requested URL intended to specify the action method is being assigned to the id route variable, which is then ignored when the action method is selected. By default, only the value of the controller variable and the verb attribute are considered when an action method is selected. To demonstrate the effect this causes, I have added a new action method to the Today controller, as shown in Listing 20-7.

Listing 20-7. Adding a New Action Method to the TodayController.cs File

using System;
using System.Web.Http;

namespace Dispatch.Controllers {

    public class TodayController : ApiController {

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

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

If you start the application and click the Get Day button, you will see a 500 (Internal Server Error) message reported. The F12 developer tools will allow you to look at the HTTP response sent by the web service, which includes this message:

Multiple actions were found that match the request

The response from the web service also contains a stack trace, so you may have to dig around to see the error message. The error arises because Web API can’t work out which of the action methods the request is intended for. It is impossible to differentiate between the action methods when only the controller routing variable and the verb specified by the attributes are available with which to make a decision.

Image Tip  I explain how to deal with errors properly in Chapter 25.

Creating a Custom Route Template

The HttpGet attribute—and the other verb attributes—is useful when the action methods in a controller are distinctive enough that the selection process can tell them apart, but a better solution to this problem is to define a custom route that has a template that uses all of the information in the URL sent by the client. Listing 20-8 shows the route I defined in the WebApiConfig.cs file.

Listing 20-8. Defining a Custom Route in 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: "IncludeAction",
                routeTemplate: "api/{controller}/{action}"
            );

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

        }
    }
}

Image Caution  The route that I have added in Listing 20-8 contains a common problem that prevents requests to the Products controller from working correctly. I explain what the problem is and how to avoid it in Chapter 22.

I have used the simplest version of the MapHttpRoute extension method, which requires only a name and a routing template. My new route defines a template that matches all of the segments in the URL sent by the client and captures the last segment as a route variable called action. This is one of the special variables—along with controller—that are used in the action method selection process, and it is assumed to contain the name of the action method that will receive the request. If the route data contains an action value, then it is used in preference to the HTTP verb to select the action method.

Image Tip  Route template segments usually match exactly one URL segment, but you can make the last segment in a template match multiple URL segments by prefixing it with an asterisk, such as {*catchall}. This feature isn’t often needed in web services because the request URL generally contains the segments needed to target the controller (and, optionally, the action method) and the data required for parameter binding (as described in Part 2).

The URL routing system evaluates routes in the order in which they are defined in the HttpRouteCollection, and the evaluation process stops as soon as a route is found that matches the current request. The MapHttpRoute method appends new routes to the end of the collection, which means that I must define my new route before the default one to ensure it is asked to route the requests from the client.

SPECIFYING ROUTE PARAMETER NAMES

You will notice that I used the C# named parameter feature in Listing 20-8 to denote which argument was the route name and which is the template, like this:

...
config.Routes.MapHttpRoute(
    name: "IncludeAction",
    routeTemplate: "api/{controller}/{action}"
);
...

This is just a convention, and I could have achieved the same effect by calling the MapHttpRoute method with normal parameters, like this:

...
config.Routes.MapHttpRoute("IncludeAction", "api/{controller}/{action}");
...

Using named parameters is helpful because some of the arguments required to define complex routes look similar, and making it clear which arguments are which makes the purpose of the route more obvious to someone reading your code. Ensuring routes work properly can be a troublesome process in large projects, and it is a good idea to make routes as clear as possible—specifying parameter names can help make the purpose and function of a route more obvious.

You can see the effect of the new route by starting the application, using the browser to navigate to the /Home/Today URL, and clicking the Get Day button. The client will send a request to the /api/today/dayofweek URL, which will be matched by the route I defined in Listing 20-8. The route template will create route data variables called controller and action—corresponding to the variable segments—with the values today and dayofweek. The action method selection process will invoke the DayOfWeek action method defined by the Today controller, which results in Figure 20-4.

9781484200865_Fig20-04.jpg

Figure 20-4. The effect of defining a custom route

UNDERSTANDING THE URL PREFIX

The convention is to prefix Web API URLs with /api, which is why the route templates I define in this chapter begin with a fixed /api segment. You don’t have to follow this convention, but you need to understand why it exists and what the impact of ignoring it will be.

The URL routing feature is available across all the technologies in the ASP.NET family and is implemented as part of the ASP.NET platform as a module. (For details of modules and how they work, see my Pro ASP.NET MVC Platform book, published by Apress.)

Web API has its own implementation of the routing system, but when the application is hosted by IIS—which is required when using the MVC framework as well—then the Web API routes are consolidated with the MVC routes into a single collection.

The order in which the Web API and MVC framework routes are arranged depends on the Application_Start method defined in the Global.asax file. The default is that the Web API routes are set up first, as follows:

...
void Application_Start(object sender, EventArgs e) {
    AreaRegistration.RegisterAllAreas();
    GlobalConfiguration.Configure(WebApiConfig.Register);
    RouteConfig.RegisterRoutes(RouteTable.Routes);
}
...

You can change the order so that the MVC routes are defined first if you prefer. Whichever way the routes are set up, you must ensure that requests are routed to the right part of the application—and that is where the /api prefix helps, by defining a fixed segment that clearly denotes web service requests and allows them to be captured by the Web API routes.

If you stop using a prefix, then you must ensure that your routes are specific enough to capture the requests intended for the Web API controllers without matching requests intended for the MVC controllers. That requires careful route planning and lots of testing.

If you just want to deliver a web service without prefixes, then reverse the order of the routing configuration statements in the Global.asax file and use an /mvc prefix for requests that are intended for MVC controllers.

Controlling Route Matching

Defining a custom route has fixed the problem with my new client code, but it has done so by causing another kind of problem. To see what has happened, start the application, using the browser to navigate to /Home/Index and clicking the Get One button. You will see a 404 (Not Found) message displayed, as shown in Figure 20-5.

9781484200865_Fig20-05.jpg

Figure 20-5. Receiving an error

Clicking the Get One button causes the client to request the following URL:

/api/products/2

This URL is matched by the route template of the route defined in Listing 20-8.

...
routeTemplate: "api/{controller}/{action}"
...

The route matches the request and generates route data that contains controller and action variables whose values are products and 2, respectively. The controller variable is fine—it contains the name of the Web API controller that the request was intended for. The problem is with the action variable, which is given special meaning and causes the action method selection process to look for an action method called 2. Since there is no such method, Web API produces a 404 (Not Found) response.

The URL routing system doesn’t know about the significance of individual segments or segment variables (it doesn’t even know that the action method selection process gives special meaning to the controller and action variables), so it diligently locates a route whose route template matches the request and uses it to produce routing data.

In the sections that follow, I’ll show you different techniques for controlling the way that routes match requests, allowing for both greater specificity (matching fewer requests) and greater generality (matching more requests). Table 20-10 puts controlling route matching in context.

Table 20-10. Putting Controlling Route Matching in Context

Question

Answer

What is it?

The range of requests that a route will match can be changed by applying optional segments, default values, and constraints.

When should you use it?

Controlling route matching can be useful in a complex application where it is difficult to direct requests to the correct controller and action method.

What do you need to know?

If you rely heavily on defaults and constraints to match requests, then it may be worth reconsidering the design of the application. Complex route configurations are rarely required in a Web API application and can suggest a structural problem that might be addressed by simplifying and consolidating the web service controllers.

Using Routing Data Default Values

Route data defaults are a flexible feature that allows you to supplement the data extracted from the request URL in order to control the route matching and controller/action selection process. In the following sections, I show you how to use default values to restrict—and broaden—the range of URLs that a route will match.

Using Segment Defaults to Restrict Matches

The most direct way to limit the set of URLs that a route will match is to increase the number of fixed segments. For my example route, I can stop it matching requests for other controllers by including the name of the controller in a static segment, as shown in Listing 20-9.

Listing 20-9. Fixing the Controller Segment in the Custom Route in 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: "IncludeAction",
                routeTemplate: "api/today/{action}",
                defaults: new { controller = "today" }
            );

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

I removed the controller variable segment and replaced it with a fixed segment that means the route template will match only URLs that start with /api/today. The controller selection process—which I described in Chapter 19—still requires a value for the controller routing variable, so I have used the defaults parameter to define a set of values that should be used for routing data if there is no value in the URL.

...
defaults: new { controller = "today" }
...

Defaults are specified with a dynamic object with properties that correspond to variables to be added to the routing data. In this case, there is only one property—controller—and it is set to today so that the selection process will route matching requests to the Today controller class.

Using Optional Segments to Widen Matches

Default values can also be used to widen the set of URLs that a route will match by denoting optional segments. This allows a route template to match URLs that don’t contain a corresponding segment, as shown in Listing 20-10.

Listing 20-10. Using Optional Segments in the WebApiConfig.cs File

...
config.Routes.MapHttpRoute(
    name: "IncludeAction",
    routeTemplate: "api/today/{action}/{day}",
    defaults: new {
        controller = "today",
        day = RouteParameter.Optional
    }
);
...

I have defined a new variable segment that will define a routing variable called day, and I have defined a corresponding default property that is set to the RouteParameter.Optional value.

This allows my custom route to match URLs such as /api/today/dayofweek/1 (which contains a day segment) and /api/today/dayofweek (which contains no day segment).

Simply broadening the range of URLs that are matched isn’t useful in its own right, but the presence of route data variables is taken into account when selecting an action method. In Listing 20-11, you can see a new action method I defined on the Today controller.

Listing 20-11. Defining a New Action Method in the TodayController.cs File

using System;
using System.Web.Http;

namespace Dispatch.Controllers {

    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  Notice that I still have to apply the HttpGet attribute. The action route data variable helps the action method selection process, but Web API still checks for the attribute that corresponds to the HTTP verb as a precaution before executing the method. I explain this process in detail in Chapter 22.

The new action method is also called DayOfWeek, but it defines a day parameter that corresponds to the optional segment in the custom route. When the action method is selected, the presence of a day variable in the route data will determine the version of the method chosen. If there is no day value, then the parameterless DayOfWeek method will be chosen. If there is a day value, then it will be used in the parameter binding process that I described in Part 2, and the other version of the DayOfWeek method will be used.

The simplest way to test the new action method is with Postman. If you send GET requests to /api/today/dayofweek and /api/today/dayofweek/4, you will see different days returned in the response (unless you do this on Thursday, in which case both methods will return the same value). For testing on a Thursday, request /api/today/dayofweek/5 instead.

Using Default Segment Values to Widen Matches

I used default values in the previous section to create an optional segment, which allowed a broader range of URLs to be matched to action methods. The standard use of default values is to allow a range of URLs to be mapped to a single action method by providing a value for the routing data that is used when a segment isn’t defined in the request URL. Listing 20-12 shows how I have changed the definition of the day property in the defaults object for the custom route.

Listing 20-12. Setting a Default Value for a Custom Route in the WebApiConfig.cs File

...
config.Routes.MapHttpRoute(
    name: "IncludeAction",
    routeTemplate: "api/today/{action}/{day}",
    defaults: new {
        controller = "today",
        day = 6
    }
);
...

I have specified a default value of 6. The default value is used only when the route matches a URL that doesn’t contain a day segment. The URL sent from the jQuery client I created at the start of the chapter is /api/today/dayofweek, and since there is no day segment, the default value is applied—and this has the effect of treating the request as though the URL was actually /api/today/dayofweek/6. The default value is not used when the URL contains a day segment, so the overall effect is to direct all requests whose URLs start with /api/today/dayofweek to the DayOfWeek action method that takes a parameter (the one I defined in Listing 20-11).

To test the default value, start the application, use the browser to navigate to /Home/Today, and click the Get Day button. The URL sent by the client will not contain a day segment, so the default value will be used, which means that the response from the action method will always be the name of the sixth day of the week, as shown in Figure 20-6. (As far as .NET is concerned, the week starts with Sunday, which is day zero. You may get different results depending on your calendar and locale settings.)

9781484200865_Fig20-06.jpg

Figure 20-6. The effect of a default value

Using Routing Constraints

Routing constraints allow you to narrow the range of requests that a route will match by adding additional checks beyond matching the routing template. In the sections that follow, I’ll show you the different ways in which constraints can be applied.

Image Caution  Use constraints only to control route matching and not to perform validation of the data values that will be used as action method parameters or by the parameter binding process I described in Part 2. Validating the data that is passed to action methods is the job of the model validation process, which I describe in Chapter 18. Using ­routing ­constraints to perform validation will cause the client to receive a 404 (Not Found) response for requests that contain bad data, which is confusing to the user because their client will have targeted a valid URL but will have done so with ­unsuitable data. Model validation allows you to reject requests and provide information about what the problems with the data are.

Understanding Constraints

Constraints are expressed using implementations of the IHttpRouteConstraint interface, which is defined in the System.Web.Http.Routing namespace. Here is the definition of the interface:

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

namespace System.Web.Http.Routing {

    public interface IHttpRouteConstraint {

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

The IHttpRouteConstraint interface defines the Match method, which is passed arguments required to constrain the match: the HttpRequestMessage object that represents the request, the IHttpRoute object that is trying to match the request, the name of the parameter that the constraint is being applied to, and a dictionary containing the data matched from the request. The final parameter is an HttpRouteDirection value, which is used to indicate whether the route is being applied to an incoming request or being used to generate an outgoing URL. The response from the Match method determines whether the route can match the request; a result of true allows a match, and a result of false prevents matching.

Creating a Custom Constraint

My goal in this section is to create a constraint that will match or reject requests based on the user-agent header sent by the client. Listing 20-13 shows the contents of the UserAgentConstraint.cs file that I added to the Infrastructure folder.

Listing 20-13. The Contents of the UserAgentConstraint.cs File

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

namespace Dispatch.Infrastructure {

    public class UserAgentConstraint : IHttpRouteConstraint {
        private string requiredUA;

        public UserAgentConstraint(string agentParam) {
            requiredUA = agentParam.ToLowerInvariant();
        }

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

            return request.Headers.UserAgent
                .Where(x =>
                    x.Product != null && x.Product.Name != null &&
                    x.Product.Name.ToLowerInvariant().Contains(requiredUA))
                .Count() > 0;
        }
    }
}

This constraint receives a constructor argument that is used to match user-agent strings. When the Match method is called, I get the value of the User-Agent header through the HttpRequestMessage object and check to see whether it contains the target string.

Image Tip  You can see an example of a constraint that operates on a segment variable in Chapter 21.

To demonstrate the use of the constraint, I have defined two routes in the WebApiConfig.cs file, as shown in Listing 20-14.

Listing 20-14. Defining Routes 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.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "ChromeRoute",
                routeTemplate: "api/today/DayOfWeek",
                defaults: new { controller = "today", action = "dayofweek"},
                constraints: new { useragent = new UserAgentConstraint("Chrome") }
            );

            config.Routes.MapHttpRoute(
                name: "NotChromeRoute",
                routeTemplate: "api/today/DayOfWeek",
                defaults: new { controller = "today", action = "daynumber" }
            );

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

As I noted at the start of this section, constraints should be used only to control whether a request matches a route—and not to perform validation of data that is going to be passed to the action method. Constraints work best when you use them to select between related routes, such as the ones in Listing 20-14. All requests to the /api/today/dayofweek URL are routed to the Today controller, but requests made from the Chrome browser are directed to the DayOfWeek action method, while all other clients are directed to the DayNumber action method.

You can see the effect by starting the application and using two browsers (one of which must be Chrome and one of which must not be Chrome) to navigate to the /Home/Today URL; then click the Get Day button. The response sent by the web service will be different for each browser, as shown by Figure 20-7, which illustrates Chrome and Internet Explorer.

9781484200865_Fig20-07.jpg

Figure 20-7. Using a custom constraint

Using the Built-in Constraints

The System.Web.Http.Routing.Constraints namespace contains classes that provide a range of built-in constraints. Table 20-11 lists the constraint classes.

Table 20-11. The Built-in Route Constraint Classes

Name

Description

AlphaRouteConstraint

Matches a route when the segment variable contains only alphabetic characters.

BoolRouteConstraint

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

DateTimeRouteConstraint

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

DecimalRouteConstraint
DoubleRouteConstraint
FloatRouteConstraint
IntRouteConstraint
LongRouteConstraint

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

HttpMethodConstraint

Matches a route when the request has been made with a specific verb. (This class is defined in the System.Web.Http.Routing namespace.)

MaxLengthRouteConstraint
MinLengthRouteConstraint

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

MaxRouteConstraint
MinRouteConstraint

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

RangeRouteConstraint

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

RegexRouteConstraint

Matches a route when the segment variable matches a regular expression.

Image Caution  I don’t want to endlessly labor the point, but these constraint classes make it easy to validate data in the wrong place, generating 404 (Not Found) errors that will confuse the client application and the user. See Chapter 18 for details of the model validation process, which can be used to return meaningful errors when the data sent by the client cannot be used.

In Listing 20-15, you can see how I have used the RegExpRouteConstraint class to allow the route to match a limited range of controller names.

Listing 20-15. Using a Built-in Constraint in the WebApiConfig.cs File

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

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

            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "ChromeRoute",
                routeTemplate: "api/today/{action}",
                defaults: new { controller = "today" },
                constraints: new {
                    useragent = new UserAgentConstraint("Chrome"),
                    action = new RegexRouteConstraint("daynumber|othermethod")
                }
            );

            config.Routes.MapHttpRoute(
                name: "NotChromeRoute",
                routeTemplate: "api/today/DayOfWeek",
                defaults: new { controller = "today", action = "daynumber" }
            );

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

I have added an action variable segment to the route template and used the RegexRouteConstraint class to limit the range of values that will match the route to daynumber and othermethod.

Image Tip  There is no othermethod action defined by the Today controller. As I explained earlier, the routing system has no insight into the data it extracts from the request, and this extends to constraints and default values. The URL system doesn’t know that special attention is paid to the action route variable by the action method selection process and so has no means—or interest—in ensuring that the constrained values are useful.

Notice that I have assigned the RegexRouteConstraint object to a property called action in the dynamic object used to set the constraints property. This is how you tell the routing system which route data variable the constraint applies to.

The effect of my constraint is to prevent the route matching the request sent by the client I created at the start of the chapter if Chrome is used—that’s because there is no combination of user-agent and URL that the client can produce that will match the combined constraints. As a consequence, all requests made from the client in Chrome to the Today controller will be matched by the NotChromeRoute and directed to the DayNumber action, as shown in Figure 20-8.

9781484200865_Fig20-08.jpg

Figure 20-8. The effect of a route constraint

Summary

In this chapter, I explained how URL routing fits into the dispatch process and showed you how to create convention-based routes to match requests. I showed you how basic matching is configured with a routing template and changed through default values and constraints. In the next chapter, I describe how to create direct routes, where the route is specified by applying attributes to controllers or action methods.

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

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