ASP.NET MVC 4 is a framework for building scalable, standards-based web applications using well-established design patterns and the power of ASP.NET and the .NET Framework. Two powerful features of ASP.NET MVC 4 are Areas and WebApi. Areas let you partition Web applications into smaller functional groupings, while WebApi is a platform for building RESTful applications. Unfortunately, combining both features is not supported by ASP.NET MVC 4. In this blog, I discuss these limitations and present a possible solution.

Background

The WebApi and Areas features play an important role in the project I am currently working on. In this project, a web application is developed for multiple types of end-users. Areas are used to create separate frontends for each type of end-user. WebApi is used as part of an interaction framework (knockoutjs) that enriches the user experience. Below is a list of relevant design decisions that were made:

  • The main MVC application resides in the root of the solution.
  • All administrator functionality resides in a separate area.
  • Each external party has its own area.
  • Each area, including the root, constitutes a well separated functional block. Functionality from one area may not be exposed to another area. This is to prevent unauthorized access of data.
  • Each area, including the root, has its own RESTfull API (WebApi).

During the development of this web application, I encountered an important limitation of WebApi when used in conjunction with Areas.

Routing and WebApi

Both regular and WebApi calls use ASP.NET MVC’s routing mechanism to translate HTTP requests to the appropriate controller action. However, only regular calls support areas, while WebApi calls are “arealess”. As a result, WebApi controllers in different areas are actually accessible from all areas. Additionally, having multiple WebApi controllers with identical names in different areas will produce an exception:

Multiple types were found that match the controller named ‘clients’. This can happen if the route that services this request (‘api/{controller}/{id}’) found multiple controllers defined with the same name but differing namespaces, which is not supported.

The request for ‘clients’ has found the following matching controllers:
MvcApplication.Areas.Administration.Controllers.Api.ClientsController
MvcApplication.Controllers.Api.ClientsController

The error message pretty much sums up the problem: ASP.NET MVC 4 RC does not support the partitioning of WebApi controllers across areas.

IHttpControllerSelector

The culprit is the DefaultHttpControllerSelector which is ASP.NET MVC’s default implementation of the IHttpControllerSelector interface. This class is responsible for selecting the appropriate IHttpController (the interface implemented by ApiController), when provided with a HTTP request message. At the heart of the DefaultHttpControllerSelector lies the HttpControllerTypeCache. This class runs through all assemblies that are used by the application and caches all types that implement the IHttpController. The SelectController method of the DefaultHttpControllerSelector uses this cache to lookup a matching type for the given controller name. This operation can end in three different manners:

  • No matching types were found, which results in an HttpStatus.NotFound (404).
  • One matching type was found, which is returned by the method and ASP.NET MVC continues to process the request.
  • Multiple matches were found, which results in an exception similar to one displayed earlier.

In search for a solution

Fortunately, through the power of Inversion of Control, developers can inject their own implementation of IHttpControllerSelector. In a related blog by Andrew Malkov, he attempts to tackle the problem by creating a custom implementation called AreaHttpControllerSelector.

This class allows area specific WebApi controllers to co-exist, provided one makes a minor modification to the WebApi routes. In order to function, a default route parameter called “area” must be added to the HttpRoute definition in the AreaRegistration file.

context.Routes.MapHttpRoute(
    name: "Administration_DefaultApi",
    routeTemplate: "Administration/api/{controller}/{id}",
    defaults: new { area = "Administration", id = RouteParameter.Optional }
);

Unfortunately, adding this extra parameter introduces a new limitation: Querystring parameters on WebApi calls no longer function. E.g. GET /Administration/api/clients will work, but GET /Administration/api/clients?firstname=john will result in a 404.

Part of the problem lies in the manner in which AreaRegistration is used to define routes. Consider the AdministrationAreaRegistration below:

public class AdministrationAreaRegistration : AreaRegistration
{
    public override string AreaName
    {
        get
        {
            return "Administration";
        }
    }

    public override void RegisterArea(AreaRegistrationContext context)
    {
        context.Routes.MapHttpRoute(
            name: "Administration_DefaultApi",
            routeTemplate: "Administration/api/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );
            
        context.MapRoute(
            "Administration_default",
            "Administration/{controller}/{action}/{id}",
            new { action = "Index", id = UrlParameter.Optional }
        );
    }
}

The first route defines how ApiContollers can be reached, while the second route defines how regular controllers can be reached. Both registrations use a different method for registering the route in order to differentiate between normal calls and WebApi calls. Routes registered through MapHttpRoute are meant for WebApi controllers while routes registered through MapRoute are meant for regular controllers.

Note that MapHttpRoute is called on the Routes collection, whereas MapRoute is called on the AreaRegistrationContext itself. This implies that there is a difference between the default MapRoute and the one provided by the AreaRegistrationContext.

After digging through the sourcecode of ASP.NET MVC, I found that the most notable difference is that the MapRoute of AreaRegistrationContext incorporates the AreaName into the route’s metadata. Specifically, the value of the AreaName property is added to the route’s DataTokens.

Solution – Part 1

I created a MapHttpRoute extension method for the AreaRegistrationContext that performed a similar operation as the AreaRegistrationContext.MapRoute method.

public static class AreaRegistrationContextExtensions
{
    public static Route MapHttpRoute(this AreaRegistrationContext context, string name, string routeTemplate)
    {
        return context.MapHttpRoute(name, routeTemplate, null, null);
    }

    public static Route MapHttpRoute(this AreaRegistrationContext context, string name, string routeTemplate, object defaults)
    {
        return context.MapHttpRoute(name, routeTemplate, defaults, null);
    }

    public static Route MapHttpRoute(this AreaRegistrationContext context, string name, string routeTemplate, object defaults, object constraints)
    {
        var route = context.Routes.MapHttpRoute(name, routeTemplate, defaults, constraints);
        if (route.DataTokens == null)
        {
            route.DataTokens = new RouteValueDictionary();
        }
        route.DataTokens.Add("area", context.AreaName);
        return route;
    }
}

To use the new extension method, remove the Routes property from the call chain:

context.MapHttpRoute(
	name: "Administration_DefaultApi",
	routeTemplate: "Administration/api/{controller}/{id}",
	defaults: new { id = RouteParameter.Optional }
);

Now both the regular routes and the WebApi routes have knowledge of their corresponding area.

Solution – Part 2

The second part of the solution is to create an implementation of IHttpControllerSelector that actually uses the area name. I took the AreaHttpControllerSelector class from Andrew Malkov’s blog post and used it as a base for my own solution.

namespace MvcApplication.Infrastructure.Dispatcher
{
    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Globalization;
    using System.Linq;
    using System.Net.Http;
    using System.Web.Http;
    using System.Web.Http.Controllers;
    using System.Web.Http.Dispatcher;

    public class AreaHttpControllerSelector : DefaultHttpControllerSelector
    {
        private const string AreaRouteVariableName = "area";

        private readonly HttpConfiguration _configuration;
        private readonly Lazy<ConcurrentDictionary<string, Type>> _apiControllerTypes;

        public AreaHttpControllerSelector(HttpConfiguration configuration)
            : base(configuration)
        {
            _configuration = configuration;
            _apiControllerTypes = new Lazy<ConcurrentDictionary<string, Type>>(GetControllerTypes);
        }

        public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
        {
            return this.GetApiController(request);
        }

        private static string GetAreaName(HttpRequestMessage request)
        {
            var data = request.GetRouteData();
            if (data.Route.DataTokens == null)
            {
                return null;
            } 
            else 
            {
                object areaName;
                return data.Route.DataTokens.TryGetValue(AreaRouteVariableName, out areaName) ? areaName.ToString() : null;
            }
        }

        private static ConcurrentDictionary<string, Type> GetControllerTypes()
        {
            var assemblies = AppDomain.CurrentDomain.GetAssemblies();

            var types = assemblies
                .SelectMany(a => a
                    .GetTypes().Where(t =>
                        !t.IsAbstract &&
                        t.Name.EndsWith(ControllerSuffix, StringComparison.OrdinalIgnoreCase) &&
                        typeof(IHttpController).IsAssignableFrom(t)))
                .ToDictionary(t => t.FullName, t => t);

            return new ConcurrentDictionary<string, Type>(types);
        }

        private HttpControllerDescriptor GetApiController(HttpRequestMessage request)
        {
            var areaName = GetAreaName(request);
            var controllerName = GetControllerName(request);
            var type = GetControllerType(areaName, controllerName);

            return new HttpControllerDescriptor(_configuration, controllerName, type);
        }

        private Type GetControllerType(string areaName, string controllerName)
        {
            var query = _apiControllerTypes.Value.AsEnumerable();

            if (string.IsNullOrEmpty(areaName))
            {
                query = query.WithoutAreaName();
            }
            else
            {
                query = query.ByAreaName(areaName);
            }

            return query
                .ByControllerName(controllerName)
                .Select(x => x.Value)
                .Single();
        }
    }

    public static class ControllerTypeSpecifications
    {
        public static IEnumerable<KeyValuePair<string, Type>> ByAreaName(this IEnumerable<KeyValuePair<string, Type>> query, string areaName)
        {
            var areaNameToFind = string.Format(CultureInfo.InvariantCulture, ".{0}.", areaName);

            return query.Where(x => x.Key.IndexOf(areaNameToFind, StringComparison.OrdinalIgnoreCase) != -1);
        }

        public static IEnumerable<KeyValuePair<string, Type>> WithoutAreaName(this IEnumerable<KeyValuePair<string, Type>> query)
        {
            return query.Where(x => x.Key.IndexOf(".areas.", StringComparison.OrdinalIgnoreCase) == -1);
        }

        public static IEnumerable<KeyValuePair<string, Type>> ByControllerName(this IEnumerable<KeyValuePair<string, Type>> query, string controllerName)
        {
            var controllerNameToFind = string.Format(CultureInfo.InvariantCulture, ".{0}{1}", controllerName, AreaHttpControllerSelector.ControllerSuffix);

            return query.Where(x => x.Key.EndsWith(controllerNameToFind, StringComparison.OrdinalIgnoreCase));
        }
    }
}

If you want to learn more about the technical details of the solution, I suggest you read Andrew’s excellent blog post first. The most significant modifications are:

  • Changed the GetAreaName method in order to retrieve the area name from the DataTokens property rather than the RouteData.
  • Added support for “arealess” WebApi controllers (e.g. those that reside in the root) to the GetControllerType method.
  • Removed the fallback mechanism from the SelectController method. The original implementation would call the SelectController method of the base-class in case GetControllerType failed to produce a result. I preferred an approach where the responsibility of successful controller selection resided in AreaHttpControllerSelector.

Finally, to inject the new AreaHttpControllerSelector class, the following line must be added to the Application_Start method in the Global.asax.cs

GlobalConfiguration.Configuration.Services.Replace(typeof(IHttpControllerSelector), new AreaHttpControllerSelector(GlobalConfiguration.Configuration));

After these modifications everything worked as expected!

Closing notes

The AreaHttpControllerSelector class presented in this blog post was designed to tackle a specific limitation of the DefaultHttpControllerSelector. However, the new implementation lacks several features which the default implementation has:

  • The AreaHttpControllerSelector searches the current loaded assemblies for the appropriate controllers, while the DefaultHttpControllerSelector also searches all referenced assemblies.
  • The DefaultHttpControllerSelector uses a much more elaborate mechanism for caching the WebApi controllers. The performance impact can be notable when the web application is loaded for the first time (cold-boot time). There is no difference in performance in subsequent requests. For more info, see this blog post by Alexander Beletsky.
  • The DefaultHttpControllerSelector throws more meaningful exceptions when no or multiple WebApi controllers are found.

Hope you enjoyed reading this blog post! In my next installment, I will show how you can get ASP.NET MVC to automagically generate knockoutjs bindings for your forms.

UPDATE: 6 september 2012

Several developers have contacted me about a scenario they encountered where the DataTokens property of the route variable is null. My implementation assumes that the DataTokens property is always initialized and will not function properly if this property is null. This behavior is most likely caused by recent changes in the ASP.NET MVC framework and may be actually be a bug in the framework. I’ve updated my code to handle this scenario.

32 thoughts on “ASP.NET MVC 4 RC: Getting WebApi and Areas to play nicely

  1. I think you have a typo in your AreaRegistrationContextExtensions. Last method should call MapRoute and not MapHttpRoute.

    • Thank you for your reply Chris. The method call is actually correct. The purpose of the AreaRegistrationContextExtensions is not to wrap MapHttpRoute around MapRoute, but to provide a version of MapHttpRoute that is Area-aware. MapHttpRoute and MapRoute are methods of the HttpRouteCollection. AreaRegistrationContext has a wrapped version of MapRoute, but no MapHttpRoute. My extension methods add MapHttpRoute to the AreaRegistrationContext.

  2. Trying this out in the RTM and it doesn’t seem to work for me :( Looks like route.DataTokens is null. Any ideas?

    • Well, I feel silly. Just added:

      route.DataTokens = new RouteValueDictionary();

      Right before and it’s working great now :) Great article!

      • Thank you for your reply, Brian. I have not encountered that problem before. Looking at the source code of ASP.NET MVC 4, I see a change that was made a while ago that may be the cause:


        HttpWebRoute route = new HttpWebRoute(routeTemplate, HttpControllerRouteHandler.Instance)
        {
        Defaults = CreateRouteValueDictionary(defaults),
        Constraints = CreateRouteValueDictionary(constraints),
        DataTokens = new RouteValueDictionary()
        };

        ..was changed into..


        HttpRouteValueDictionary defaultsDictionary = new HttpRouteValueDictionary(defaults);
        HttpRouteValueDictionary constraintsDictionary = new HttpRouteValueDictionary(constraints);
        HostedHttpRoute httpRoute = (HostedHttpRoute) GlobalConfiguration.Configuration.Routes.CreateRoute(routeTemplate, defaultsDictionary, constraintsDictionary, dataTokens: null, handler: handler) ;
        Route route = httpRoute.OriginalRoute;

        Note that after the change, dataTokens is initialized to null instead of an empty dictionary. I’m not sure if this is the culprit because I would expect the underlying HttpRoute (or Route base class) to always initialize its dictionaries.

  3. Great article! Thanks for documenting this so well. I also ran into an issue in the GetAreaName method of AreaHttpControllerSelector where the DataTokens collection had not been initialized when there was a controller that was not in an area. I added a simple check to see if it was null before querying it.

    if (data.Route.DataTokens == null)
    {
    return null;
    }

    That the DataTokens collection is not initialized seems like a bug in the framework.

  4. GREAT! I have been going around and around trying to get Areas to work! But I’m confused. I have the second part of your solution done… but solution 1: Where do I put AreaRegistrationContextExtensions
    ?
    No matter where I put it I get an error on the context.Routes.MapHttpRoute… something like ‘System.Web.Routing.RouteCollection’ does not contain a definition for MapHttpRoute.

    Thanks!

    • Hello, Dave. The AreaRegistrationContextExtensions are extension methods for the AreaRegistrationContext. Extension methods enable you to “add” methods to existing types without creating a new derived type, recompiling, or otherwise modifying the original type. You can place the class anywhere in your solution, just make sure you add the right using statement to your AreaRegistration file.

      E.g. Say you have placed your AreaRegistrationContextExtensions in the MvcApplication.Utility namespace, and your AreaRegistration resides in the MvcApplication.Areas.Administration namespace. In order to use the extension methods from in your AreaRegistration, add using MvcApplication.Utility to your AreaRegistration file.

      I hope that this information will help you. Good luck!

      • Thanks so much… but I’m missing something.

        Here is my AreaRegistrationContextExtensions.cs:

        using System.Web.Mvc;
        using System.Web.Routing;

        namespace RouteTest.Extensions
        {
        public static class AreaRegistrationContextExtensions
        {
        public static Route MapHttpRoute(this AreaRegistrationContext context, string name, string routeTemplate)
        {
        return context.MapHttpRoute(name, routeTemplate, null, null);
        }

        public static Route MapHttpRoute(this AreaRegistrationContext context, string name, string routeTemplate, object defaults)
        {
        return context.MapHttpRoute(name, routeTemplate, defaults, null);
        }

        public static Route MapHttpRoute(this AreaRegistrationContext context, string name, string routeTemplate, object defaults, object constraints)
        {
        var route = context.Routes.MapHttpRoute(name, routeTemplate, defaults, constraints);
        if (route.DataTokens == null)
        {
        route.DataTokens = new RouteValueDictionary();
        }
        route.DataTokens.Add(“area”, context.AreaName);
        return route;
        }
        }
        }

        And here is my JobsAreaRegistration.cs

        using System.Web.Mvc;
        using RouteTest.Extensions;

        namespace RouteTest.Areas.Jobs
        {
        public class JobsAreaRegistration : AreaRegistration
        {
        public override string AreaName
        {
        get
        {
        return “Jobs”;
        }
        }

        public override void RegisterArea(AreaRegistrationContext context)
        {
        context.Routes.MapHttpRoute(
        name: “Jobs_defaultApi”,
        routeTemplate: “api/Jobs/{controller}/{id}”,
        defaults: new { id = RouteParameter.Optional }
        );

        context.MapRoute(
        “Jobs_default”,
        “Jobs/{controller}/{action}/{id}”,
        new { action = “Index”, id = UrlParameter.Optional },
        new[] { “RouteTest.Areas.Jobs.Controllers” }
        );
        }
        }
        }

        The line: “var route = context.Routes.MapHttpRoute(name, routeTemplate, defaults, constraints);” produces an error so the dang thing wont compile.

        II now understand the concept… just cannot get there from here.

        Thanks for your help!!! If you have a sample project, that would send me on my way.

        D

        • Hi Dave.

          In the line with context.Routes.MapHttpRoute, you need to remove the Routes part so it becomes context.MapHttpRoute. This will pickup the MapHttpRoute of the AreaRegistrationContextExtensions.

          Success!

  5. OK… works great… except… when I come in the first time and the routes are initialized, the route.DataTokens is set to a new RouteValueDictionary. All seems to be good and the first call works like a charm. In fact all calls to that area work fine. But when I make a call to another area, the DataTokens retains the first area. eg… first call is “api/Jobs/Send” then DataToken[“area”] = “Jobs”… next call goes to “api/Reports/Read” and the DataToken[“area”] = “Jobs” still. I think that something needs to reinitialize DataTokens… Am I missing something? Or did something change since RC?

    Thanks!

    • OK… some new information

      The function:
      private static string GetAreaName(HttpRequestMessage request)
      {
      var data = request.GetRouteData();
      object areaName;
      return data.Route.DataTokens.TryGetValue(AreaRouteVariableName, out areaName) ? areaName.ToString() : null;
      }

      Is returning the wrong value. ‘data’ seems to contain the right values, but the data.Route.DataTokens contains the wrong values. I’m probably not setting something correctly so I’m sorry if I’m wasting time.

      Thanks!

      • Hi Dave,

        In our project we are still on RC, but we will soon be upgrading to RTM. I’ll investigate the issue once we’ve made the transition. In my personal projects I’ve already noticed some subtle behavioral differences between RC and RTM, so I suspect this may have something to do with it.

        Greetings

        Martin

  6. Hi Dave,

    I’m getting the following exception in GetControllerType:

    Value cannot be null. Parameter name: controllerType

    Any ideas? Thanks!

    • Hi Jonathan,

      In what class in what method and on what line do you see this error? How are your routes configured? Is this happening in an area request or an area-less request? What is the name of the controller you are trying to reach? In what namespace does it reside?

      Answers to these questions will help you and me to solve this issue :-)

      Martin

      • Hi,

        My mistake! I incorrectly defined the routes – maybe I should have paid a bit more attention to the tutorial…

        Thanks for your quick response.

        • Hi Jonathan,

          Glad to hear you solved the problem on your own. Yes, this blog has a lot of information to digest. The routing mechanism of .NET is an intricate system with plenty of ‘gotcha’s’.

          Martin

  7. Pingback: Getting WebApi and Areas to play nicely | Jisku.com - Developers Network

  8. Trying to call an api controller in ~/Controllers/ that is called the same as a controller in ~/Areas/Administration/Controllers, and it’s always sending me to the ~/Areas/Administration/Controllers class instead of going to ~/Controllers/{controller}. Is there something special I need to do to my root to get this to work? How should the default controller mapping look for WebAPI with this functionality?

  9. I’m finding that I have to define the route in each [X]AreaRegistration class like so:
    context.MapHttpRoute(
    name: “Whatever_Api_default”,
    routeTemplate: “whatever/api/{controller}/{id}”,
    defaults: new { area = “Whatever”, id = RouteParameter.Optional }
    );

    Is that expected? It would be nice to define the area api route globally instead of each time a new area is added something like this:
    routes.MapHttpRoute(
    name: “DefaultAreaApi”,
    routeTemplate: “{area}/api/{controller}/{id}”,
    defaults: new { id = RouteParameter.Optional }
    );

    Is this not possible?

  10. This solution has been great for me so far with one minor issue… optional query string parameters. Typically I was able to use nullable types for parameters that I want to be optional in the query string, but now when one of those parameters is not in the query string, i get the error: “No action was found on the controller ” that matches the request.”

    Is anyone else experiencing this as well and/or have any suggestions?

      • I solved my problem, and it was (mostly) unrelated. Some of my WebAPI actions are non-RESTful (e.g., /product/DoSomething) and so my routeTemplate needed to be “api/{contoller}/{action}/{id}”, not just “api/{controller}/{id}”. After making that change, my default parameters worked as expected again.

  11. The solution does not work with the global definition of api route. In that case the datatokens are still null.

    config.Routes.MapHttpRoute(
    name: “AdminApi”,
    routeTemplate: “api/{area}/{controller}/{id}”,
    defaults: new { id = RouteParameter.Optional }

    For this to work we have to define the api route in the area registration only where you init the data tokens to an empty dictionary in case it is null

    its a great little piece of code. thanks for the work.

  12. Pingback: ASP .net MVC 4 Web API + Areas sample solution | Lemoncode

  13. Just wanted to say thanks for the excellent blog. I have been struggling with this for over a day, and this did the trick. It is extremely handy to have APIs in their own areas…I started running into situations where I was having name conflicts between MVC controllers and API controllers.

      • That’s a MVC-feature, not a WebAPI-feature. Although ASP.NET MVC and ASP.NET WebAPI share a lot of concepts, they are two different frameworks with seperate implementations. The System.Web.Mvc.RouteAreaAttribute will only work for System.Web.Mvc.Controller types and not System.Web.Http.ApiController types.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>