Customizing ASP.NET Core’s Route Constraints and Model Binding

Posted January 18, 2017

I’m building a website using ASP.NET Core. The site serves the same content types, but with unique content, for different cities. The site also serves content that isn’t city-specific, like the site’s “About” and “Contact” pages, and its administrative interface.

URLs for city-specific content should include the city name for SEO benefits, as seen in these examples:

After exploring different ways to handle the city-specific content, I implemented the feature by customizing ASP.NET Core’s route constraints and model binding. Read on for an explanation and example code of how it works.

NewController.cs Index method excerpt:

This approach results in a couple of problems. First, there hasn’t been any validation of the cityInfo string parameter when the action method is called. What if the city’s name is misspelled, the state abbreviation is missing, or an unsupported city is specified?

Validation is clearly needed, which exposes this approach’s second problem. We’ll have to validate the passed string parameter in every action method that delivers city-specific content. If the parameter’s value is valid, we will also need to perform whatever operations are necessary to fetch the city’s unique content.

We can reduce the amount of code in our action methods to a few lines by placing the validation and data retrieval code in a separate helper class/method as seen below, but this approach still gets out of hand as our site grows.

Validating URL Fragments With Custom Route Constraints

Our first step is validating the cityInfo URL fragment. We do this by creating a custom implementation of ASP.NET Core’s IRouteConstraint interface, then adding it to the constraint map specified in ASP.NET Core’s route options.

ASP.NET Core’s IRouteConstraint interface:

namespace Microsoft.AspNetCore.Routing { // // Summary: // /// Defines the contract that a class must implement in order to check whether // a URL parameter /// value is valid for a constraint. /// public interface IRouteConstraint { // // Summary: // /// Determines whether the URL parameter contains a valid value for this constraint. // /// // // Parameters: // httpContext: // An object that encapsulates information about the HTTP request. // // route: // The router that this constraint belongs to. // // routeKey: // The name of the parameter that is being checked. // // values: // A dictionary that contains the parameters for the URL. // // routeDirection: // /// An object that indicates whether the constraint check is being performed // /// when an incoming request is being handled or when a URL is being generated. // /// // // Returns: // true if the URL parameter contains a valid value; otherwise, false. bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection); } }

The IRouteConstraint interface specifies a single Match method that must be implemented. The method returns a boolean result, which indicates if the URL fragment being tested passed the constraint’s evaluation.

There are a number of parameters passed to IRouteConstraint’s Match method. We’ll review the ones we use in our custom constraint implementation, beginning with the routeKey string parameter. It specifies the route fragment that is being tested. In the case of the route we added earlier, routeKey will be assigned a value of “cityInfo”.

The next parameter we use is the values RouteValueDictionary parameter. Its a dictionary that maps a key of type string to a value of type object. The key is the name specified in a route. The value references the value found in the provided URL. An example may help to clarify this one.

…produce this RouteValueDictionary instance:

By using the value of routeKey (“cityInfo”) as our key, the values dictionary returns “new-york-ny” as the cityInfo URL fragment.

Those are the only method parameters that I need to access in my implementation of the IRouteConstraint interface. If you need to reference any of the other passed parameters in your implementation, see the Microsoft documentation for more details. Next, lets take a look at my implementation of the interface.

While my list of supported cities may eventually be stored in a database so they can be easily modified by a site administrator, I’m using a static Cities dictionary in this example. The different supported cities’ URL fragments are mapped to custom data class instances. We’ll see how those data instances are provided to the action methods later in this article.

As mentioned before, the RouteValueDictionary class maps a string to a generic object. For my custom route constraint implementation, I need to cast the generic object to a string. I also want to ignore upper/lowercase variations of the string content. Here’s the part of the code that does that.

values[routeKey]?.ToString().ToLowerInvariant()

In the above code, null is returned if no key is found with the name specified by the routeKey parameter, or if one was found but its assigned value was null. If this is the case, the ToString() and ToLowerInvariant methods aren’t called, thanks to C# 6.0’s null-conditional operator (“?”). Otherwise, the generic object instance returned by the values RouteValueDictionary is explicitly converted to a string, which is then explicitly converted to lowercase content.

Once the city name to be tested has been retrieved, the static Cities dictionary’s ContainsKey method is used to return the boolean value expected from the Match method. It will return true if a key that matches the specified city is found in the dictionary, or false if it isn’t.

Registering Our Custom Route Constraint

The next step is letting ASP.NET Core know about the new constraint. We take care of that by adding an entry to the constraint map found in ASP.NET Core’s routing options. This is done in your project’s ConfigureServices method in your Startup class:

With those changes in place, our custom constraint will be called upon to verify the cityInfo URL fragment. If the constraint returns false, the route isn’t considered to be a match. ASP.NET Core then proceeds to the next assigned route if one exists.

Extending ASP.NET Core’s Model Binding Process

Now that we have our URL validation out of the way, we need to tackle the second part of how I want my solution to work, which is directly passing the matching ICityInfo data instance as a parameter to action methods, like this example.

In my opinion, this version is a huge win over our earlier implementation of the Index action method. Lets review how to make this work in ASP.NET Core.

ASP.NET Core’s default model binding can convert a variety of URL elements. This Microsoft documentation is a great starting point if you’re not familiar with everything it can do. However, I didn’t see a way of passing my Cities dictionary’s matching ICityInfo entries as an action method parameter to methods that needed to access it.

Fortunately, Microsoft has made ASP.NET Core very extensible. We can add the desired support for passing our ICityInfo instances to action methods by implementing and registering our own implementations of ASP.NET Core’s IModelBinderProvider and IModelBinder interfaces similarly to how we handled the IRouteConstraint interface earlier in this article.

In the above code, I added a couple of sanity checks, first throwing an exception if the context wasn’t provided—it should always be provided. Next, I verify the desired data type is an implementor of my ICityInfo interface before returning an instance of my custom model binder.

Again, I added a couple of sanity checks, verifying the context was provided and that a matching ICityInfo instance was found. A matching ICityInfo instance will always be found if our custom CityRouteConstraint constraint was used. This check protects against the constraint not being applied in the route.

The key part of the method is the assignment of the matching city info to the context’s Result property. With that assignment in place, ASP.NET Core’s model binding mechanism will pass the ICityInfo instance to any action method that requests it as a parameter. Or at least it will once I register my custom implementation of the IModelBinderProvider interface.

Registering My Implementation of the IModelBinderProvider Interface

The final step in making things work as I want them requires revisiting our Startup class’s ConfigureServices method.

Startup.cs ConfigureServices method excerpt:

I insert my custom binder provider at the beginning of the ModelBinderProviders list. While this isn’t required for things to work as desired, I expect the custom provider will be used to handle the majority of my site’s requests, so it may improve the site’s performance. Of course, that will need to be load tested to know for sure.

Thats a Wrap

That does it! The routing of city-specific content is now working on the site as I wanted it to. Here are screen shots of this article’s sample code showing results for Los Angeles and New York City.

I can imagine other uses for these techniques as well. For example, lets say you’re building a site that includes identical sections for every department in your organization. Each section may have news, events, FAQ pages, discussion forums, etc., with clean URLs to access them like “http://example.com/marketing/faq” and “http://example.com/engineering/events”. Using the techniques described here will handle this for you in a clean, reusable way.

I hope you enjoyed the article. Do you know a better way to address this with ASP.NET Core? Please let me know if so. I’m always interested in learning new things! You’re also welcomed to subscribe to my blog if you’d like for articles like this one to be delivered to your inbox.

Share this:

Related

Reader Interactions

Primary Sidebar

About Me

I’m a mobile app architect using Xamarin and Flutter technologies to build great mobile apps for Android and iOS devices. I’ve worked with both large enterprises and small firms to create award winning products.

Recent Tweets

This chart of StackOverflow questions over time is startling even to me: it shows the rapid growth of Dart and its rebirth as a language alongside the fast rise of Flutter as a UI framework. Exciting! @dart_langpic.twitter.com/salh…

Curious about the files inside the release build of an app bundle? Check out @chinmaygarde's post. He breaks down how the iOS application bundle is put together, and also has some details on Android apps.
Read it here ↓ bit.ly/2Mp1Szo

I wish I knew how to use the StreamBuilder in @flutterio earlier on.
Here's how to use the RefreshIndicator along with the StreamBuilder to refresh a list pulled from an API endpoint
blog.khophi.co/using…

How one startup boosted their productivity by a factor of 10: “Flutter simply blows away the competition - you will not notice the difference between it and a 'truly' native app. I wish I knew a year ago, what I know now.” linkedin.com/pulse/h…#Flutter#mobileappdevelopment