Web API Custom Routing Constraints

Have you ever tried to restrict a Web API route in the way to allow only certain kind of values for specific segments? You will probably have and the most common restriction someone will find out on the web is the following route:

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

The above code registers a route where the id parameter supposed to be either Optional or digits. If you used the above route I’ m sure you were disappointed when you realized that the RouteParameter.Optional default value for the id, simply stopped working. In other works the above code is wrong. For example if you had a Web API controller named “ProductsController” the following would give you a error.

/api/products/

This one though would work just fine.

/api/products/3

The problem is that in the way we registered the route, adding a constraint for digit number overwrites the default contraint which means that it always waits for a digit value for the “id” segment. So what we can do to solve this? The solution is to create our own Web API Custom Constraint. We are going to show how this works with the following simple scenario: We will have a CountriesController in our application and when we pass the “/api/Countries/” URL all available countries must be returned. On the other hand when we pass the “/api/Countries/XX” where XX means two letters (and only letters), the controller must return the respective country with the XX abbreviation. In other words we will support both an RouteParameter.Optional constraint for the “Abbreviation” segment parameter and a Custom constraint which will restrict the abbreviation segment to allow only 2 letters (not 1, not 3). Let’s start.
Create a new Empty ASP.NET Web Application named WebAPIRoutingConstraints. First we will create our model classes. Add a new C# class file named “Country” and paste the following code.

namespace WebAPIRoutingConstraints
{
    public class Country
    {
        public string Name { get; set; }
        public string Abbreviation { get; set; }
    }
}

Let’s create a context now so that we can have some mock Country objects. Add a new C# class named “CountryContext” and paste the following simple code.

public class CountryContext
    {
        private readonly ConcurrentDictionary<int, Country> _countries; 
        public CountryContext()
        {
            _countries = new ConcurrentDictionary<int, Country>();
            _countries.TryAdd(1, new Country { Name = "GREECE", Abbreviation = "GR"  });
            _countries.TryAdd(2, new Country { Name = "France", Abbreviation = "FR" });
            _countries.TryAdd(3, new Country { Name = "United Kingdom",Abbreviation = "UK" });
            _countries.TryAdd(4, new Country { Name = "India", Abbreviation = "IN" });
            _countries.TryAdd(5, new Country { Name = "Canada", Abbreviation = "CA" });
        }

        public IEnumerable<Country> All
        {
            get { return _countries.Values; }
        }

        public Country GetCountry(string abbreviation)
        {
            return _countries.FirstOrDefault(p => 
                string.Equals(p.Value.Abbreviation,abbreviation, StringComparison.OrdinalIgnoreCase)).Value;
        }
    }

We only need two functions for our example to work, one to return all available countries and another that will return a specific country for an abbreviation value given. Now that we do have our model, we need to add the required Web API libraries so that we can actually use and host a Web API service. Right click your project and open the NuGet Packages.. Search online for Web API and make sure you install the Web API Core libraries.
webapicontraint
Right click your project and add a Web API Controller class named “CountriesController”. Paste the following code.

public class CountriesController : ApiController
    {
        public static CountryContext context = new CountryContext();

        // GET api/<products>
        public IEnumerable<Country> Get()
        {
            return context.All;
        }

        // GET api/<products>/5
        public Country Get(string abbreviation)
        {
            return context.GetCountry(abbreviation);
        }
    }

Of course, at this time if you try to invoke an action of that controller it will fail since we haven’t register yet any route. Add a Global Configuration file (leave the default Global.asax name) and add the following code into the Application_Start method.

protected void Application_Start(object sender, EventArgs e)
        {
            var config = GlobalConfiguration.Configuration;

            config.Routes.MapHttpRoute("DefaultHttpRoute", "api/{controller}/{abbreviation}",
                defaults: new { abbreviation = RouteParameter.Optional }
             );
        }

Build and run your solution. Request either all available countries or a specific countries given an abbreviation. You should see the controller work just fine but of course you can pass whatever value you want for the abbreviation segment.
webapicontraint_01
webapicontraint_02
webapicontraint_03
Let’s add now the constraint for the abbreviation. Change slightly the route registration as follow.

config.Routes.MapHttpRoute("DefaultHttpRoute", "api/{controller}/{abbreviation}",
                defaults: new { abbreviation = RouteParameter.Optional },
                constraints: new { abbreviation = @"[A-Za-z]{2}" }
             );

We only allow a 2 letter abbreviation now, so the following URL should return an error.
webapicontraint_04
The problem is that a URL with no abbreviation value will return also an error, that is the defaults: new { abbreviation = RouteParameter.Optional } doesn’t actually work.
webapicontraint_05
Let’s implement now our Custom Constraint. Add a new C# class named CustomRegExConstraint and paste the following code.

public class CustomRegExConstraint : IHttpRouteConstraint
    {
        private readonly string _regEx;

        public CustomRegExConstraint(string regEx)
        {
            if (string.IsNullOrEmpty(regEx))
            {
                throw new ArgumentNullException("regEx");
            }

            _regEx = regEx;
        }

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

            if (values[parameterName] != RouteParameter.Optional)
            {

                object value;
                values.TryGetValue(parameterName, out value);
                string pattern = "^(" + _regEx + ")$";
                string input = Convert.ToString(
                    value, CultureInfo.InvariantCulture);

                return Regex.IsMatch(input, pattern, RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
            }

            return true;
        }
    }

If you want to create a Custom Route constraint, you create a class that implements the IHttpRouteConstraint interface. This interface has only one method to be implemented, the “Match”.

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

The “string parameterName” parameter will hold the parameter name for which this constraint will be added, that is the abbreviation. The “IDictionary values” is a dictionary that holds the segments names with their respective values.
webapicontraint_06
When we pass now the http://localhost:13646/api/countries the

values[parameterName] != RouteParameter.Optional

returns false hence the Match method returns true and we do have a route match. If you do pass a value for the abbreviation segment, the above code will return true and eventually will check if you passed a two letter abbreviation. If yes then you have a route match, otherwise it will return false and you won’t reach the controller. Now that you have created the Constraint you have to bind it to the “Abbreviation” segment. Do this by changing one more time your route registration.

config.Routes.MapHttpRoute("DefaultHttpRoute", "api/{controller}/{abbreviation}",
                defaults: new { abbreviation = RouteParameter.Optional },
                constraints: new { abbreviation = new CustomRegExConstraint(@"[A-Za-z]{2}") }
            );

Build and run your application. Make sure that all work in the way they supposed to. That’s it, this is how you can create and add a custom constraint for your Web API routes. You can download the project we created from here. I hope you enjoyed the post, follow the blog to get notified for new posts!



Categories: ASP.NET

Tags: , ,

13 replies

  1. A very well wriiten article – thanks for that. You might also want to look at the new technique Microsoft is adopting for MVC v5, attribute-based routing: http://conficient.wordpress.com/2013/08/19/asp-net-mvc-attribute-routing/

  2. I was going to say the same thing as the previous commenter. I’m actively developing a Web API project and we used nuget to add attribute routing in the very beginning of the project. Attribute routing provides constraints for just about every value type. I did have to create my own constraint for “short” but it was simple, as the attribute routing library is open source. See http://aspnetwebstack.codeplex.com/wikipage?title=Attribute%20routing%20in%20Web%20API

  3. Thank you both of you, very good stuff.

  4. I don’t even know how I ended up here, but I thought this post was good.
    I don’t know who you are but definitely you’re going to a
    famous blogger if you are not already 😉 Cheers!

  5. Great blog you have got here.. It’s hard to find excellent writing like
    yours these days. I seriously appreciate individuals like you!
    Take care!!

  6. Yes! Finally someone writes about guide to thesis proposal writing sample.

  7. Really nice article! Thanks a lot Chris for sharing this.

  8. Great article Chris. Thanks a lot of sharing this!

  9. Why don’t you just make the regex optional? e.g.,

    constraints: new { abbreviation = “([A-Za-z]{2}){0,1}” }

    • Hi Pete, indeed you have a point, your suggestion would also work for this particular problem, but I was trying to explain how to use Custom Routing constraints not just to solve the particular problem.

      Thanks a lot for sharing it with us though.

  10. I loved as much as you will receive carried oout right here.The sketch is attractive, your authored subject matter stylish.
    nonetheless, you command get bought an impatiencxe over that you wish bbe delivering the following.
    unwell unquestionably come further formerly again since exactly the same nearly a lot often inside
    case you shield this hike.

  11. At the start of the article, the constraint doesn’t overwrite RouteParameters.Optional, it just contradicts it. Change the plus to an asterisk in the regex of the constraint and they will play nicely together.

  12. Hi Chris!
    I need some explanation about how below code is working. I mean how RouteParameter.Optional is compared to values[parameterName] and what is values[parameterName] value

    values[parameterName] != RouteParameter.Optional

Leave a Reply to Christos S. Cancel reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: