Imagine that you have a page that is designed to show the details of a product. Following convention, you create a page named Details.cshtml in a folder named Product within the Pages folder. You want visitors to reach the page using the URL domain.com/product/details/{productname}
where the {productname}
is the actual name of a specific product (suitably slugified). So you add a route template accordingly:
@page "{productname}"
Now you can retrieve the RouteData
value for productname, and use it to perform a database query. This works fine, but what you soon begin to realise is that sometimes, the database query doesn't return a result. When you review your logs, you see that the value being passed to the database query is not what you expect to see in your URLs. It might be part of the product name, or it might have some extra characters added, or indeed it might bear no resemblance to anything in your database at all. There are countless ways in which links to your site can get broken when they are being shared, or stored by a poorly written bot.
What you really could do with is some way to prevent the wasted processing that these database look-ups for non-existent values incur, and also inform the requester that the page they are looking for doesn't exist. You want to return a 404 HTTP status code.
The solution can be implemented as a custom constraint. Then the routing system will take care of ensuring that the user gets the correct response.
An Example Look-up Service
The constraint will work by matching incoming route values to existing product names. The product names need to obtained from the database. Obviously you don't want to do this for every request - that would defeat the object of the exercise. So you will use caching as part of your strategy. For the purposes of demonstration, however, the service will just return a List
public void ConfigureServices(IServiceCollection services) { ... services.AddTransient<IProductService, ProductService>(); }
The Constraint
Constraints implement the IRouteConstraint
interface, which defines one method: Match
. This is where the logic that determines whether a value satisfies the constraint is placed. The method returns a bool
indicating success:
In this example, the ProductService
is injected into the constructor. The Match
method returns true if there is an entry in the RouteValueDictionary
with the specified key that matches any in the database.
Registering And Using The Constraint
The constraint needs to be registered with the routing system. This is done in the ConfigureServices
method in Startup
:
services.Configure(options =>
{
options.ConstraintMap.Add("product", typeof(ProductConstraint));
});
The entry added to the RoutOption.ConstraintMap
consists of a string
as the key and a Type
as the value. The key is used to to identify the constraint when you apply it to a route value parameter:
@page "{productname:product}"
A valid value in the URL will result in a successful request:
Whereas if you request the Details page without passing in an existing value, the framework returns a 404:
Summary
The built-in set of route constraints are most likely enough for the majority of applications, but if you need to build your own custom constraint, you just need to implement IRouteConstraint
and register your implementation with your application's service container. As you can see from the example above, this is not very difficult to do.