ASP.NET MVC: Custom Constraints – Matching the User Agent String


I have been really diving into routing with ASP.NET MVC and inevitably I came to the topic of custom constraints on routes. One of the really nice applications of custom constraints is matching a string contained within the client’s user agent string sent along with the request from the browser. For those that don’t know, the user agent is a string that is sent from the browser with information such as the type of browser used to make the request, the operating system of the client, and for most mobile phone browsers, the type of phone making the request. Check out useragentstring.com to see a detailed explanation of your browser’s user agent string.

If you have a web application that should render differently on a mobile phone browser, say the iPhone, you can check the user agent string to see if the request is coming from an iPhone and if it is, you can direct it accordingly. To do such a thing in ASP.NET MVC, first we need to create a UserAgentConstraint class implementing the IRouteConstraint interface that will check to see if a given string is found within the client’s user agent string.

public class UserAgentConstraint : IRouteConstraint
{
    public string _stringInUserAgent;
    public bool _caseSensitive;

    public UserAgentConstraint(string stringInUserAgent, bool caseSensitive)
    {
        this._stringInUserAgent = stringInUserAgent;
        this._caseSensitive = caseSensitive;
    }

    public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
    {
        if (httpContext.Request.UserAgent == null)
            return false;

        // Check to see if the indicate string is found within the client's user agent string
        if (_caseSensitive)
            return httpContext.Request.UserAgent.Contains(_stringInUserAgent);

        // If not case sensitve, make everything lowercase and then check    
        return httpContext.Request.UserAgent.ToLower().Contains(_stringInUserAgent.ToLower());
    }
}

When a constraint is checked, the Match method is called and returns whether or not the constraint is satisfied.

Now when defining the route, simply set the constraint for the user agent as a new instance of the UserAgentConstraint class matching the word iphone.

public static void RegisterRoutes(RouteCollection routes)
{
    ...

    routes.MapRoute(
        null, // Route name
        "Home", // URL to match
        new { controller = "Home", action = "IndexIPhone" }, // Parameter defaults
        new { userAgent = new UserAgentConstraint("iphone", false) }    // Constraints
    );
}

The above route, a rather dull example, simply directs the request to an action method in the Home controller named IndexIPhone where presumably the same logic resides for non-iPhone requests. In reality, you would probably want to separate these controllers into separate namespaces. Fortunately, you can also specify another RouteValueDictionary that lets the application know which namespace to look in when searching for the controller, but we will leave that for another time.

ASP.NET MVC: Register a Route with a Parameter Constraint and a Default Value Defined in the Action Method


This weekend I was playing around with the routing configuration for an ASP.NET MVC project and ran into an interesting situation. I created a simple test project with a ProductsController containing and action method named List and as well its corresponding view. The List action method accepted an integer parameter named page that would indicate which page of the catalog to display.

I wanted to setup the routes so that the url ~/Catalog (not specifying any page parameter) would be mapped to the List action method so that the first page of the catalog would be displayed. Conversely, if the url ~/Catalog/2 was used, the List action method would be called and the second page would be displayed. This is actually a pretty simple task; all you need to do is set the page parameter in the route as optional as follows:

routes.MapRoute(null, 	// Route Name
                "Catalog/{page}",	// Url to match
                new { controller = "Products", action = "List", page = UrlParameter.Optional } 	// Default values
                );

This simple ASP.NET MVC 101 route works great but the one issue is that this route will match the url ~/Catalog/abc as well and attempt to cast the string ‘abc’ to an integer and cause the application to fail. My first thought was to simply put a constraint on the page parameter as follows to indicate that only digits could be accepted.

routes.MapRoute(null, 	// Route Name
                "Catalog/{page}",	// Url to match
                new { controller = "Products", action = "List", page = UrlParameter.Optional }, 	// Default values
                new { page = @"\d+" } 	// Parameter constraints
                );

This solved the above mentioned problem so that the url ~/Catalog/abc was no longer handled by this route. But, this created another problem, since I put the constraint on the page parameter that it had to be a digit value, this route no longer matched the url ~/Catalog. The simplest solution to this is replace the UrlParameter.Optional value assigned to the page parameter in the default values collection with the value 1 or whatever your default value is. If you do this and navigate to the url ~/Catalog, the ASP.NET MVC framework will automatically pass in the default value of 1 you supplied when register the route. But generally I like to specify the default values to my action methods using the [DefaultValue] attribute or the .NET Framework 4 syntax for specifying optional parameters as shown below.

public ActionResult List(int page = 1)
{
     // Method logic
}

Since I am looking at my controller code much more the the code used for registering routes, I am reminded that a default value is being provided to the method if one is not present in the url or the query string. Purely a personal preference, but one that I like to follow.

After much research, I concluded that there is no way to allow the user to enter both ~/Catalog and ~/Catalog/2 and to provide the default value along with the action method in a single route definition. In order to accomplish what I desired, the following two routes needed to be registered:

// Matches ~/Catalog/2
routes.MapRoute(null,	// Route Name
                "Catalog/{page}",	// Url to match
                new { controller = "Products", action = "List" },	// Default values
                new { page = @"\d+" }	// Parameter constraints
                );

// Matches ~/Catalog
routes.MapRoute(null,	// Route name
                "Catalog",	// Url to match
                new { controller = "Products", action = "List" }	// Default values
                );

If anyone has a solution to this using only one route, please let me know.

Follow

Get every new post delivered to your Inbox.

Join 70 other followers