Putting URL Routing in Context
Question | Answer |
---|---|
What is it? | URL routing consolidates the processing and matching of URLs, allowing components known as endpoints to generate responses. |
Why is it useful? | URL routing obviates the need for each middleware component to process the URL to see whether the request will be handled or passed along the pipeline. The result is more efficient and easier to maintain. |
How is it used? | The URL routing middleware components are added to the request pipeline and configured with a set of routes. Each route contains a URL path and a delegate that will generate a response when a request with the matching path is received. |
Are there any pitfalls or limitations? | It can be difficult to define the set of routes matching all the URLs supported by a complex application. |
Are there any alternatives? | URL routing is optional, and custom middleware components can be used instead. |
This chapter focuses on URL routing for the ASP.NET Core platform. See Part 3 for details of how the higher-level parts of ASP.NET Core build on the features described in this chapter.
Chapter Summary
Problem | Solution | Listing |
---|---|---|
Handling requests for a specific set of URLs | Define a route with a pattern that matches the required URLs | 1–6 |
Extracting values from URLs | Use segment variables | 7–10, 14 |
Generating URLs | Use the link generator to produce URLs from routes | 11–13, 15 |
Matching URLs with different numbers of segments | Use optional segments or catchall segments in the URL routing pattern | 16–18 |
Restricting matches | Use constraints in the URL routing pattern | 19–21, 23–26 |
Matching requests that are not otherwise handled | Define fallback routes | 22 |
Seeing which endpoint will handle a request | Use the routing context data | 27 |
Preparing for This Chapter
In this chapter, I continue to use the Platform project from Chapter 12. To prepare for this chapter, add a file called Population.cs to the Platform folder with the code shown in Listing 13-1.
You can download the example project for this chapter—and for all the other chapters in this book—from https://github.com/apress/pro-asp.net-core-3. See Chapter 1 for how to get help if you have problems running the examples.
The Contents of the Population.cs File in the Platform Folder
This middleware component responds to requests for /population/<city> where <city> is london, paris, or monaco. The middleware component splits up the URL path string, checks that it has the expected length, and uses a switch statement to determine it is a request for a URL that it can respond to. If the URL matches the pattern the middleware is looking for, then a response is generated; otherwise, the request is passed along the pipeline.
The Contents of the Capital.cs File in the Platform Folder
This middleware component is looking for requests for /capital/<country>, where <country> is uk, france, or monaco. The capital cities of the United Kingdom and France are displayed, but requests for Monaco, which is a city and a state, are redirected to /population/monaco.
Replacing the Contents of the Startup.cs File in the Platform Folder
Starting the ASP.NET Core Runtime
Understanding URL Routing
Each middleware component decides whether to act on a request as it passes along the pipeline. Some components are looking for a specific header or query string value, but most components—especially terminal and short-circuiting components—are trying to match URLs.
Each middleware component has to repeat the same set of steps as the request works its way along the pipeline. You can see this in the middleware defined in the prevision section, where both components go through the same process: split up the URL, check the number of parts, inspect the first part, and so on.
This approach is inefficient, it is difficult to maintain, and it breaks easily when changed. It is inefficient because the same set of operations is repeated to process the URL. It is difficult to maintain because the URL that each component is looking for is hidden in its code. It breaks easily because changes must be carefully worked through in multiple places. For example, the Capital component redirects requests to a URL whose path starts with /population, which is handled by the Population component. If the Population component is revised to support the /size URL instead, then this change must also be reflected in the Capital component. Real applications can support complex sets of URLs, and working changes fully through individual middleware components can be difficult.
URL routing solves these problems by introducing middleware that takes care of matching request URLs so that components, called endpoints, can focus on responses. The mapping between endpoints and the URLs they require is expressed in a route. The routing middleware processes the URL, inspects the set of routes and finds the endpoint to handle the request, a process known as routing.
Adding the Routing Middleware and Defining an Endpoint
The routing middleware is added using two separate methods: UseRouting and UseEndpoints. The UseRouting method adds the middleware responsible for processing requests to the pipeline. The UseEndpoints method is used to define the routes that match URLs to endpoints. URLs are matched using patterns that are compared to the path of request URLs, and each route creates a relationship between one URL pattern and one endpoint. Listing 13-5 shows the use of the routing middleware and contains a simple route.
I explain why there are two methods for routing in the “Accessing the Endpoint in a Middleware Component” section.
Using the Routing Middleware in the Startup.cs File in the Platform Folder
The IEndpointRouteBuilder Extension Methods
Name | Description |
---|---|
MapGet(pattern, endpoint) | This method routes HTTP GET requests that match the URL pattern to the endpoint. |
MapPost(pattern, endpoint) | This method routes HTTP POST requests that match to the URL pattern to the endpoint. |
MapPut(pattern, endpoint) | This method routes HTTP PUT requests that match the URL pattern to the endpoint. |
MapDelete(pattern, endpoint) | This method routes HTTP DELETE requests that match the URL pattern to the endpoint. |
MapMethods(pattern, methods, endpoint) | This method routes requests made with one of the specified HTTP methods that match the URL pattern to the endpoint. |
Map(pattern, endpoint) | This method routes all HTTP requests that match the URL pattern to the endpoint. |
There are also extension methods that set up endpoints for other parts of ASP.NET Core, such as the MVC Framework, as explained in Part 3.
Endpoints are defined using RequestDelegate, which is the same delegate used by conventional middleware, so endpoints are asynchronous methods that receive an HttpContext object and use it to generate a response. This means that all the features described in earlier chapters for middleware components can also be used in endpoints.
The routing middleware short-circuits the pipeline when a route matches a URL so that the response is generated only by the route’s endpoint. The request isn’t forwarded to other endpoints or middleware components that appear later in the request pipeline.
If the request URL isn’t matched by any route, then the routing middleware passes the request to the next middleware component in the request pipeline. To test this behavior, request the http://localhost:5000/notrouted URL, whose path doesn’t match the pattern in the route defined in Listing 13-5.
Using Middleware Components as Endpoints in the Startup.cs File in the Platform Folder
Understanding URL Patterns
Using middleware components as endpoints shows that the URL routing feature builds on the standard ASP.NET Core platform. Although the URLs that the application handles can be seen by examining the routes, not all of the URLs understood by the Capital and Population classes are routed, and there have been no efficiency gains since the URL is processed once by the routing middleware to select the route and again by the Capital or Population class in order to extract the data values they require.
Matching URL Segments
URL Path | Description |
---|---|
/capital | No match—too few segments |
/capital/europe/uk | No match—too many segments |
/name/uk | No match—first segment is not capital |
/capital/uk | Matches |
Using Segment Variables in URL Patterns
The URL pattern used in Listing 13-6 uses literal segments, also known as static segments, which match requests using fixed strings. The first segment in the pattern will match only those requests whose path has capital as the first segment, for example, and the second segment in the pattern will match only those requests whose second segment is uk. Put these together and you can see why the route matches only those requests whose path is /capital/uk.
Using Segment Variables in the Startup.cs File in the Platform Folder
Useful RouteValuesDictionary Members
Name | Description |
---|---|
[key] | The class defines an indexer that allows values to be retrieved by key. |
Keys | This property returns the collection of segment variable names. |
Values | This property returns the collection of segment variable values. |
Count | This property returns the number of segment variables. |
ContainsKey(key) | This method returns true if the route data contains a value for the specified key. |
There are some reserved words that cannot be used as the names for segment variables: action, area, controller, handler, and page.
The RouteValuesDictionary class is enumerable, which means that it can be used in a foreach loop to generate a sequence of KeyValuePair<string, object> objects, each of which corresponds to the name of a segment variable and the corresponding value extracted from the request URL. The endpoint in Listing 13-7 enumerates the HttpRequest.RouteValues property to generate a response that lists the names and value of the segment variables matched by the URL pattern.
When processing a request, the middleware finds all the routes that can match the request and gives each a score, and the route with the lowest score is selected to handle the route. The scoring process is complex, but the effect is that the most specific route receives the request. This means that literal segments are given preference over segment variables and that segment variables with constraints are given preference over those without (constraints are described in the “Constraining Segment Matching” section later in this chapter). The scoring system can produce surprising results, and you should check to make sure that the URLs supported by your application are matched by the routes you expect.
If two routes have the same score, meaning they are equally suited to routing the request, then an exception will be thrown, indicating an ambiguous routing selection. See the “Avoiding Ambiguous Route Exceptions” section later in the chapter for details of how to avoid ambiguous routes.
Refactoring Middleware into an Endpoint
Depending on the Route Data in the Capital.cs File in the Platform Folder
The indexer returns an object value that is cast to a string using the as keyword. The listing removes the statements that pass the request along the pipeline, which the routing middleware handles on behalf of endpoints.
The use of the segment variable means that requests may be routed to the endpoint with values that are not supported, so I added a statement that returns a 404 status code for countries the endpoint doesn’t understand.
Depending on Route Data in the Population.cs File in the Platform Folder
Updating Routes in the Startup.cs File in the Platform Folder
These changes address two of the problems I described at the start of the chapter. Efficiency has improved because the URL is processed only once by the routing middleware and not by multiple components. And it is easier to see the URLs that each endpoint supports because the URL patterns show how requests will be matched.
Generating URLs from Routes
Naming a Route in the Startup.cs File in the Platform Folder
The WithMetadata method is used on the result from the MapGet method to assign metadata to the route. The only metadata required for generating URLs is a name, which is assigned by passing a new RouteNameMetadata object, whose constructor argument specifies the name that will be used to refer to the route. In Listing 13-11, I have named the route population.
Naming routes helps to avoid links being generated that target a route other than the one you expect, but they can be omitted, in which case the routing system will try to find the best matching route. You can see an example of this approach in Chapter 17.
Generating a URL in the Capital.cs File in the Platform Folder
Changing a URL Pattern in the Startup.cs File in the Platform Folder
The URL routing system supports a feature called areas, which allows separate sections of the application to have their own controllers, views, and Razor Pages. I have not described the areas feature in this book because it is not widely used and, when it is used, it tends to cause more problems than it solves. If you want to break up an application, then I recommend creating separate projects.
Managing URL Matching
The previous section introduced the basic URL routing features, but most applications require more work to ensure that URLs are routed correctly, either to increase or to restrict the range of URLs that are matched by a route. In the sections that follow, I show you the different ways that URL patterns can be adjusted to fine-tune the matching process.
Matching Multiple Values from a Single URL Segment
Matching Part of a Segment in the Startup.cs File in the Platform Folder
The order of the segment variables shown in Figure 13-12 shows that pattern segments that contain multiple variables are matched from right to left. This isn’t important most of the time, because endpoints can’t rely on a specific key order, but it does show that complex URL patterns are handled differently, which reflects the difficulty in matching them.
This pattern has a segment that begins with the literal string red, followed by a segment variable named color. The routing middleware will correctly match the pattern against the URL path example/redgreen, and the value of the color route variable will be green. However, the URL path example/redredgreen won’t match because the matching process confuses the position of the literal content with the first part of the content that should be assigned to the color variable. This problem may be fixed by the time you read this book, but there will be other issues with complex patterns. It is a good idea to keep URL patterns as simple as possible and make sure you get the matching results you expect.
Using Default Values for Segment Variables
Using Default Values in the Startup.cs File in the Platform Folder
Matching URLs
URL Path | Description |
---|---|
/ | No match—too few segments |
/city | No match—first segment isn’t capital |
/capital | Matches, country variable is France |
/capital/uk | Matches, country variable is uk |
/capital/europe/italy | No match—too many segments |
Using Optional Segments in a URL Pattern
Using a Default Value in the Population.cs File in the Platform Folder
Using an Optional Segment in the Startup.cs File in the Platform Folder
Matching URLs
URL Path | Description |
---|---|
/ | No match—too few segments. |
/city | No match—first segment isn’t size. |
/size | Matches. No value for the city variable is provided to the endpoint. |
/size/paris | Matches, city variable is paris. |
/size/europe/italy | No match—too many segments. |
Using a catchall Segment Variable
Using a Catchall Segment in the Startup.cs File in the Platform Folder
The new pattern contains two-segment variables and a catchall, and the result is that the route will match any URL whose path contains two or more segments. There is no upper limit to the number of segments that the URL pattern in this route will match, and the contents of any additional segments are assigned to the segment variable named catchall. Restart ASP.NET Core and navigate to http://localhost:5000/one/two/three/four, which produces the response shown in Figure 13-15.
Notice that the segments captured by the catchall are presented in the form segment/segment/segment and that the endpoint is responsible for processing the string to break out the individual segments.
Constraining Segment Matching
Applying Constraints in the Startup.cs File in the Platform Folder
The URL Pattern Constraints
Constraint | Description |
---|---|
alpha | This constraint matches the letters a to z (and is case-insensitive). |
bool | This constraint matches true and false (and is case-insensitive). |
datetime | This constraint matches DateTime values, expressed in the nonlocalized invariant culture format. |
decimal | This constraint matches decimal values, formatted in the nonlocalized invariant culture. |
double | This constraint matches double values, formatted in the nonlocalized invariant culture. |
file | This constraint matches segments whose content represents a file name, in the form name.ext. The existence of the file is not validated. |
float | This constraint matches float values, formatted in the nonlocalized invariant culture. |
guid | This constraint matches GUID values. |
int | This constraint matches int values. |
length(len) | This constraint matches path segments that have the specified number of characters. |
length(min, max) | This constraint matches path segments whose length falls between the lower and upper values specified. |
long | This constraint matches long values. |
max(val) | This constraint matches path segments that can be parsed to an int value that is less than or equal to the specified value. |
maxlength(len) | This constraint matches path segments whose length is equal to or less than the specified value. |
min(val) | This constraint matches path segments that can be parsed to an int value that is more than or equal to the specified value. |
minlength(len) | This constraint matches path segments whose length is equal to or more than the specified value. |
nonfile | This constraint matches segments that do not represent a file name, i.e., values that would not be matched by the file constraint. |
range(min, max) | This constraint matches path segments that can be parsed to an int value that falls between the inclusive range specified. |
regex(expression) | This constraint applies a regular expression to match path segments. |
Some of the constraints match types whose format can differ based on locale. The routing middleware doesn’t handle localized formats and will match only those values that are expressed in the invariant culture format.
Combining URL Pattern Constraints in the Startup.cs File in the Platform Folder
Constraining Matching to a Specific Set of Values
Matching Specific Values in the Startup.cs File in the Platform Folder
The route will match only those URLs with two segments. The first segment must be capital, and the second segment must be uk, france, or monaco. Regular expressions are case-insensitive, which you can confirm by restarting ASP.NET Core and requesting http://localhost:5000/capital/UK, which will produce the result shown in Figure 13-18.
You may find that your browser requests /capital/uk, with a lowercase uk. If this happens, clear your browser history and try again.
Defining Fallback Routes
Using a Fallback Route in the Startup.cs File in the Platform Folder
The Methods for Creating Fallback Routes
Name | Description |
---|---|
MapFallback(endpoint) | This method creates a fallback that routes requests to an endpoint. |
MapFallbackToFile(path) | This method creates a fallback that routes requests to a file. |
There is no magic to fallback routes. The URL pattern used by fallbacks is {path:nofile}, and they rely on the Order property to ensure that the route is used only if there are no other suitable routes, which is a feature described in the “Avoiding Ambiguous Route Exceptions” section.
Advanced Routing Features
The routing features described in the previous sections address the needs of most projects, especially since they are usually accessed through higher-level features such as the MVC Framework, described in Part 3. There are some advanced features for projects that have unusual routing requirements, which I describe in the following sections.
Creating Custom Constraints
The Contents of the CountryRouteConstraint.cs File in the Platform Folder
Using a Custom Constraint in the Startup.cs File in the Platform Folder
Requests will be matched by this route only when the first segment of the URL is capital and the second segment is one of the countries defined in Listing 13-23.
Avoiding Ambiguous Route Exceptions
When trying to route a request, the routing middleware assigns each route a score. As explained earlier in the chapter, precedence is given to more specific routes, and route selection is usually a straightforward process that behaves predictably, albeit with the occasional surprise if you don’t think through and test the full range of URLs the application will support.
Defining Ambiguous Routes in the Startup.cs File in the Platform Folder
Breaking Route Ambiguity in the Startup.cs File in the Platform Folder
Accessing the Endpoint in a Middleware Component
As earlier chapters demonstrated, not all middleware generates responses. Some components provide features used later in the request pipeline, such as the session middleware, or enhance the response in some way, such as status code middleware.
One limitation of the normal request pipeline is that a middleware component at the start of the pipeline can’t tell which of the later components will generate a response. The routing middleware does something different. Although routes are registered in the UseEndpoints method, the selection of a route is done in the UseRouting method, and the endpoint is executed to generate a response in the UseEndpoints method. Any middleware component that is added to the request pipeline between the UseRouting method and the UseEndpoints method can see which endpoint has been selected before the response is generated and alter its behavior accordingly.
Adding a Middleware Component in the Startup.cs File in the Platform Folder
The Properties Defined by the Endpoint Class
Name | Description |
---|---|
DisplayName | This property returns the display name associated with the endpoint, which can be set using the WithDisplayName method when creating a route. |
Metadata | This property returns the collection of metadata associated with the endpoint. |
RequestDelegate | This property returns the delegate that will be used to generate the response. |
There is also a SetEndpoint method that allows the endpoint chosen by the routing middleware to be changed before the response is generated. This should be used with caution and only when there is a compelling need to interfere with the normal route selection process.
Summary
In this chapter, I introduced the endpoint routing system and explained how it deals with some common problems arising in regular middleware. I showed you how to define routes, how to match and generate URLs, and how to use constraints to restrict the use of routes. I also showed you some of the advanced uses of the routing system, including custom constraints and avoiding route ambiguity. In the next chapter, I explain how ASP.NET Core services work.