ASP.NET MVC 4 WebAPI. Support Areas in HttpControllerFactory

March 26, 2012

This article was written for ASP.NET MVC 4 Beta. If you are using Release Candidate version of ASP.NET MVC 4 then you have to read the next article.

Unfortunately DefaultHttpControllerFactory doesn't support Areas by default.
To support it you have to write your HttpControllerFactory from scratch.

In this post I will show you how you can do it.

AreaHttpControllerFactory

First of all, you have to implement IHttpControllerFactory interface:

    public class AreaHttpControllerFactory : IHttpControllerFactory
    {
        public IHttpController CreateController(HttpControllerContext controllerContext, string controllerName)
        {
            throw new NotImplementedException();
        }

        public void ReleaseController(IHttpController controller)
        {
            throw new NotImplementedException();
        }
    }

We will implement two obligatory methods CreateController and ReleaseController later.

Let's start with fields and constructor:

        private const string ControllerSuffix = "Controller";
        private const string AreaRouteVariableName = "area";

        private IHttpControllerFactory _defaultFactory;
        private HttpConfiguration _configuration;
        private Dictionary<string, Type> _apiControllerTypes;

        public AreaHttpControllerFactory(HttpConfiguration configuration)
        {
            _configuration = configuration;
            _defaultFactory = new DefaultHttpControllerFactory(configuration);
        }

Our class is wrapper around DefaultHttpControllerFactory.
I hope I don't need to explain meaning of ControllerSuffix constant. But I would like to explain the AreaRouteVariableName constant - it contains the name of the variable which we will use to specify area name  in Routes collection.

In the _apiControllerTypes we will store all the API controllers types, so let's create private property:

        private Dictionary<string, Type> ApiControllerTypes
        {
            get
            {
                if (_apiControllerTypes != null)
                {
                    return _apiControllerTypes;
                }

                var assemblies = AppDomain.CurrentDomain.GetAssemblies();

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

                return _apiControllerTypes;
            }
        }

This code, just takes all the API controllers types from all of your assemblies, and store it inside the dictionary, where the key is FullName of the type and value is the type itself.
Of course we will set this dictionary only once. And then just use it.

Now we are ready to implement the one of our main methods. I will start with ReleaseController method:

        public void ReleaseController(IHttpController controller)
        {
            _defaultFactory.ReleaseController(controller); 
        }

Easy.

The last method will do all the "magic" for us:

        public IHttpController CreateController(HttpControllerContext controllerContext, string controllerName)
        {
            var controller = GetApiController(controllerContext, controllerName);
            return controller ?? _defaultFactory.CreateController(controllerContext, controllerName);
        }

Easy as well. :)
And the method which will find the controller for us:

        private IHttpController GetApiController(HttpControllerContext controllerContext, string controllerName)
        {
            if (!controllerContext.RouteData.Values.ContainsKey(AreaRouteVariableName))
            {
                return null;
            }

            var areaName = controllerContext.RouteData.Values[AreaRouteVariableName].ToString().ToLower();
            if (string.IsNullOrEmpty(areaName))
            {
                return null;
            }

            var type = ApiControllerTypes.Where(t => t.Key.ToLower().Contains(string.Format(".{0}.", areaName)) && t.Key.EndsWith(string.Format(".{0}{1}", controllerName, ControllerSuffix), StringComparison.OrdinalIgnoreCase)).Select(t => t.Value).FirstOrDefault();
            if (type == null)
            {
                return null;
            }

            return CreateControllerInstance(controllerContext, controllerName, type);
        }

So, if an area variable is specifying in RouteData.Values and this variable is not null or not empty, then it tries to find the controller in the ApiControllerTypes by full name of the controller where the full name contains area's name surrounded by "." (e.g. ".Admin.") and ends with controller name + controller suffix (e.g. UsersController).
If the controller type is found, then it will calls CreateControllerInstance method. Otherwise, the method will return null.

And the code for CreateControllerInstance:

        private IHttpController CreateControllerInstance(HttpControllerContext controllerContext, string controllerName, Type controllerType)
        {
            var descriptor = new HttpControllerDescriptor(_configuration, controllerName, controllerType);
            controllerContext.ControllerDescriptor = descriptor;
            var controller = descriptor.HttpControllerActivator.Create(controllerContext, controllerType);
            controllerContext.Controller = controller;
            return controller;
        }

Registering AreaHttpControllerFactory

The next thing you have to do is to say to your application to use this controller factory instead of DefaultHttpControllerFactory. And fortunately it is really easy - just add one additional line to the end of Application_Start method in Glogal.asax file:

        protected void Application_Start()
        {
            // your default code

            GlobalConfiguration.Configuration.ServiceResolver.SetService(typeof(IHttpControllerFactory), new AreaHttpControllerFactory(GlobalConfiguration.Configuration));
        }

That's all.

Using AreaHttpControllerFactory

If you did everything right, now you can forget about that "nightmare" code. And just start to use it!

You have to add new HttpRoute to your AreaRegistration.cs file:

        public override void RegisterArea(AreaRegistrationContext context)
        {
            context.Routes.MapHttpRoute(
                name: "Admin_Api",
                routeTemplate: "api/admin/{controller}/{id}",
                defaults: new { area = AreaName, id = RouteParameter.Optional }
            );

            // other mappings
        }

That's all. Good luck, and have a nice day.

Recommended content

Comments

Leave your comment