Tuesday 7 May 2013

How to Select a Web API Controller at Runtime?

You can implement IHttpControllerSelector interface and select a controller at runtime.

You may want to do this when you're supporting multiple versions of the API for backward compatibility.

The version number might be in the header or in the url e.g. api/v1/events/1.


using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Dispatcher;
using System.Linq;

namespace WebApiSecurityDemo.Model.Versioning
{
    public class VersionControllerSelector : DefaultHttpControllerSelector
    {
        private readonly HttpConfiguration _configuration;

        private Dictionary _apiControllerTypes;

        private const string ApiContainerAssembly = "WebApiSecurityDemo.WebApi";

        public VersionControllerSelector(HttpConfiguration configuration)
            : base(configuration)
        {
            _configuration = configuration;
            this.VersionLocation = VersionLocationStrategy.FromUrl;
        }

        private Dictionary ApiControllerTypes
        {
            get
            {
                return _apiControllerTypes ?? (_apiControllerTypes = GetControllerTypes());
            }
        }

        public enum VersionLocationStrategy
        {
            FromUrl,
            FromHeader
        }

        public VersionLocationStrategy VersionLocation { get; set; }
        
        public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
        {                
            return GetVersionedController(request) ?? base.SelectController(request);
        }

        private HttpControllerDescriptor GetVersionedController(HttpRequestMessage request)
        {
            var controllerName = base.GetControllerName(request);

            var version = ExtractVersion(request);
        
            if (version == null)
            {
                // throw bad request - version is compulsory
                return null;
            }

            var type = GetControllerTypeByVersion(version, controllerName);

            if (type == null)
            {
                // throw bad request - not supported
                return null;
            }

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

        private string ExtractVersion(HttpRequestMessage request)
        {
            return this.VersionLocation == VersionLocationStrategy.FromUrl
                ? ExtractVersionFromUrl(request)
                : ExtractVersionFromHeader(request);
        }

        private static string ExtractVersionFromUrl(HttpRequestMessage request)
        {
            Match match = Regex.Match(request.RequestUri.PathAndQuery, @"/v(?\d+)/", RegexOptions.IgnoreCase);

            return match.Success ? match.Groups["version"].Value : null;
        }

        private static string ExtractVersionFromHeader(HttpRequestMessage request)
        {
            var version = request.Headers.Accept.Select(i => i.Parameters.Single(s => s.Name == "version").Value).ToList();

            return !version.Any() ? null : version.First();
        }

        private Type GetControllerTypeByVersion(string version, string controllerName)
        {
            var versionToFind = string.Format("V{0}", version.ToLower());

            var controllerNameToFind = string.Format("{0}.{1}{2}", versionToFind, controllerName, ControllerSuffix);

            return ApiControllerTypes.Where(t => t.Key.ToLower().Contains(versionToFind.ToLower())
                                                 && t.Key.EndsWith(controllerNameToFind, StringComparison.OrdinalIgnoreCase)).Select(t => t.Value).FirstOrDefault();
        }

        private static Dictionary GetControllerTypes()
        {
            var apiAssembly = AppDomain.CurrentDomain.GetAssemblies().Single(a => a.FullName.Contains(ApiContainerAssembly));

            return apiAssembly
                .GetTypes()
                .Where(t => !t.IsAbstract && t.Name.EndsWith(ControllerSuffix) && typeof(IHttpController).IsAssignableFrom(t))
                .ToDictionary(thisType => thisType.FullName);
        }
    }
}


then you can register the new ControllerSelector:
GlobalConfiguration.Configuration.Services.Replace(typeof (IHttpControllerSelector), new VersionControllerSelector(GlobalConfiguration.Configuration));