user7052300
user7052300

Reputation:

How to version Web API using Namespaces - ~/api/v1/products

I am trying to make a versionned API, so basically I followed this article : https://blogs.msdn.microsoft.com/webdev/2013/03/07/asp-net-web-api-using-namespaces-to-version-web-apis/

I have set some routes for my WebApi like this :

namespace WebApiApplication {
public static class WebApiConfig {
    public static void Register(HttpConfiguration config) {
        // Web API configuration and services

        // Web API routes
        config.MapHttpAttributeRoutes();

        config.Routes.MapHttpRoute(
            name: "DefaultApi",
            routeTemplate: "api/{version}/{controller}",
            defaults: new { version = "v2" }
        );

        config.Routes.MapHttpRoute(
            name: "DefaultApiWithId",
            routeTemplate: "api/{version}/{controller}/{id}",
            defaults: new { id = RouteParameter.Optional }
        );

        config.Services.Replace(typeof(IHttpControllerSelector), new NamespaceHttpControllerSelector(config));
    }
}}

My project then has a Controllers folder, which contains two folders "v1" and "v2". The controllers looks like this :

~/Controllers/v1/ProductsController.cs

namespace WebApiApplication.Controllers.v1 {
[RoutePrefix("api/v1/products")]
public class ProductsController : ApiController {
    Product[] products = new Product[] {
        new Product { UserReference = 1, Name = "product1" },
        new Product { UserReference = 2, Name = "product2" }
    };

    [HttpGet]
    [Route("")]
    public IEnumerable<Product> GetAllProducts() {
        return products;
    }

    [HttpGet]
    [Route("{userReference}")]
    public IHttpActionResult GetProducts(int userReference) {
        var res = products.Where(t => t.UserReference == userReference);

        if (res == null)
            return NotFound();

        return Ok(res);
    }
}}

~/Controllers/v2/ProductsController.cs

namespace WebApiApplication.Controllers.v2 {
[RoutePrefix("api/v2/products")]
public class ProductsController : ApiController {
    Product[] products = new Product[] {
        new Product { UserReference = "a", Name = "product1" },
        new Product { UserReference = "b", Name = "product2" }
    };

    [HttpGet]
    [Route("")]
    public IEnumerable<Product> GetAllProducts() {
        return products;
    }

    [HttpGet]
    [Route("{userReference}")]
    public IHttpActionResult GetProducts(string userReference) {
        var res = products.Where(t => t.UserReference == userReference);

        if (res == null)
            return NotFound();

        return Ok(res);
    }
}}

The only difference here between the version is the UserReference becoming a string in the V2.

Because I have the same controller name in both versions, I have to override the current "IHttpControllerSelector", to find the requested controller :

~/NamespaceHttpControllerSelector.cs

public class NamespaceHttpControllerSelector : IHttpControllerSelector {
    private const string VERSION_KEY = "version";
    private const string CONTROLLER_KEY = "controller";

    private readonly HttpConfiguration _configuration;
    private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers;
    private readonly HashSet<string> _duplicates;

    public NamespaceHttpControllerSelector(HttpConfiguration configuration) {
        _configuration = configuration;
        _duplicates = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        _controllers = new Lazy<Dictionary<string, HttpControllerDescriptor>>(InitializeControllerDictionary);
    }

    public HttpControllerDescriptor SelectController(HttpRequestMessage request) {
        IHttpRouteData routeData = request.GetRouteData();

        if (routeData == null)
            throw new HttpResponseException(HttpStatusCode.NotFound);

        string versionName = GetRouteVariable<string>(routeData, VERSION_KEY); // Here, I am always getting null

        if (versionName == null)
            throw new HttpResponseException(HttpStatusCode.NotFound);

        string controllerName = GetRouteVariable<string>(routeData, CONTROLLER_KEY);

        if (controllerName == null)
            throw new HttpResponseException(HttpStatusCode.NotFound);

        // Find a matching controller.
        string key = string.Format(CultureInfo.InvariantCulture, "{1}.{2}", versionName, controllerName);

        HttpControllerDescriptor controllerDescriptor;
        if (_controllers.Value.TryGetValue(key, out controllerDescriptor)) {
            return controllerDescriptor;
        }
        else if (_duplicates.Contains(key)) {
            throw new HttpResponseException(
                request.CreateErrorResponse(
                    HttpStatusCode.InternalServerError,
                    "Multiple controllers were found that match this request."));
        }
        else {
            throw new HttpResponseException(HttpStatusCode.NotFound);
        }
    }

    public IDictionary<string, HttpControllerDescriptor> GetControllerMapping() {
        return _controllers.Value;
    }

    private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary() {
        var dictionary = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);

        // Create a lookup table where key is "namespace.controller". The value of "namespace" is the last
        // segment of the full namespace. For example:
        // MyApplication.Controllers.V1.ProductsController => "V1.Products"
        IAssembliesResolver assembliesResolver = _configuration.Services.GetAssembliesResolver();
        IHttpControllerTypeResolver controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();

        ICollection<Type> controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);

        foreach (Type t in controllerTypes) {
            var segments = t.Namespace.Split(Type.Delimiter);

            // For the dictionary key, strip "Controller" from the end of the type name.
            // This matches the behavior of DefaultHttpControllerSelector.
            var controllerName = t.Name.Remove(t.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length);

            var key = string.Format(
                CultureInfo.InvariantCulture,
                "{0}.{1}",
                segments[segments.Length - 1],
                controllerName);

            // Check for duplicate keys.
            if (dictionary.Keys.Contains(key)) {
                _duplicates.Add(key);
            }
            else {
                dictionary[key] = new HttpControllerDescriptor(_configuration, t.Name, t);
            }
        }

        // Remove any duplicates from the dictionary, because these create ambiguous matches. 
        // For example, "Foo.V1.ProductsController" and "Bar.V1.ProductsController" both map to "v1.products".
        foreach (string s in _duplicates) {
            dictionary.Remove(s);
        }

        return dictionary;
    }

    private static T GetRouteVariable<T>(IHttpRouteData routeData, string name) {
        object result = null;

        if (routeData.Values.TryGetValue(name, out result))
            return (T) result;

        return default(T);
    }
}

The problem is in the "SelectController" method. I am always getting "null" in versionName when doing "string versionName = GetRouteVariable(routeData, VERSION_KEY);" (when I call "http://localhost:27039/api/v1/products/1" for example).

I should have the version requested in the URL instead...

My Global.asax.cs :

namespace WebApiApplication {
public class WebApiApplication : System.Web.HttpApplication {
    protected void Application_Start() {
        GlobalConfiguration.Configure(WebApiConfig.Register);
    }
}}

Upvotes: 1

Views: 3581

Answers (2)

Win
Win

Reputation: 62300

Few people has modified Mike Wasson's Original Source. I like the one used in ASP.NET Web API 2 book by Jamie Kurtz, Brian Wortman.

Since it has too many moving pieces, I created a sample project at GitHub. I personally used the similar code in few of my projects.

Remove custom routes in your RouteConfig file.

config.Routes.MapHttpRoute(
   name: "DefaultApi",
   routeTemplate: "api/{version}/{controller}",
   defaults: new { version = "v2" }
);

config.Routes.MapHttpRoute(
   name: "DefaultApiWithId",
   routeTemplate: "api/{version}/{controller}/{id}",
   defaults: new { id = RouteParameter.Optional }
);

Then, you add ApiVersionConstraint

public class ApiVersionConstraint : IHttpRouteConstraint
{
    public ApiVersionConstraint(string allowedVersion)
    {
        AllowedVersion = allowedVersion.ToLowerInvariant();
    }

    public string AllowedVersion { get; private set; }

    public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName,
        IDictionary<string, object> values, HttpRouteDirection routeDirection)
    {
        object value;
        if (values.TryGetValue(parameterName, out value) && value != null)
        {
            return AllowedVersion.Equals(value.ToString().ToLowerInvariant());
        }
        return false;
    }
}

Then, you place the RoutePrefix on a controller, and you are done.

[RoutePrefix("api/{apiVersion:apiVersionConstraint(v1)}/values")]
public class ValuesController : ApiController
{
    // GET api/v1/values
    [Route("")]
    public IEnumerable<string> Get()
    {
        return new string[] { "v1-value1", "v1-value2" };
    }

    // GET api/v1/values/5
    [Route("{id}")]
    public string Get(int id)
    {
        return "v1-value-" + id;
    }
}

Upvotes: 1

Derek
Derek

Reputation: 8630

Here is a much better solution using version constraints.

public class ApiVersionConstraint : IHttpRouteConstraint
    {
        public string AllowedVersion { get; private set; }

        public ApiVersionConstraint(string allowedVersion)
        {
            AllowedVersion = allowedVersion.ToLowerInvariant();
        }

        public bool Match(HttpRequestMessage request, IHttpRoute route, string parameterName, IDictionary<string, object> values, HttpRouteDirection routeDirection)
        {
            object value;
            if (values.TryGetValue(parameterName, out value) && value != null)
            {
                return AllowedVersion.Equals(value.ToString().ToLowerInvariant());
            }
            return false;
        }
    }

Here is a RoutePrefixAttribute for both V1 and V2

public class ApiVersion1RoutePrefixAttribute : RoutePrefixAttribute
    {
        private const string RouteBase = "api/{apiVersion:apiVersionConstraint(v1)}";
        private const string PrefixRouteBase = RouteBase + "/";

        public ApiVersion1RoutePrefixAttribute(string routePrefix)
            : base(string.IsNullOrWhiteSpace(routePrefix) ? RouteBase : PrefixRouteBase + routePrefix)
        {
        }
    }


 public class ApiVersion2RoutePrefixAttribute : RoutePrefixAttribute
        {
            private const string RouteBase = "api/{apiVersion:apiVersionConstraint(v2)}";
            private const string PrefixRouteBase = RouteBase + "/";

            public ApiVersion1RoutePrefixAttribute(string routePrefix)
                : base(string.IsNullOrWhiteSpace(routePrefix) ? RouteBase : PrefixRouteBase + routePrefix)
            {
            }
        }

Here is the NamespaceControllerSelector.

public class NamespaceHttpControllerSelector : IHttpControllerSelector
    {
        private readonly HttpConfiguration _configuration;
        private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers;

        public NamespaceHttpControllerSelector(HttpConfiguration config)
        {
            _configuration = config;
            _controllers = new Lazy<Dictionary<string, HttpControllerDescriptor>>(InitializeControllerDictionary);
        }

        public HttpControllerDescriptor SelectController(HttpRequestMessage request)
        {
            var routeData = request.GetRouteData();
            if (routeData == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            var controllerName = GetControllerName(routeData);
            if (controllerName == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            var namespaceName = GetVersion(routeData);
            if (namespaceName == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }

            var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", namespaceName, controllerName);

            HttpControllerDescriptor controllerDescriptor;
            if (_controllers.Value.TryGetValue(controllerKey, out controllerDescriptor))
            {
                return controllerDescriptor;
            }

            throw new HttpResponseException(HttpStatusCode.NotFound);
        }

        public IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
        {
            return _controllers.Value;
        }

        private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
        {
            var dictionary = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);

            var assembliesResolver = _configuration.Services.GetAssembliesResolver();
            var controllersResolver = _configuration.Services.GetHttpControllerTypeResolver();

            var controllerTypes = controllersResolver.GetControllerTypes(assembliesResolver);

            foreach (var controllerType in controllerTypes)
            {
                var segments = controllerType.Namespace.Split(Type.Delimiter);

                var controllerName = controllerType.Name.Remove(controllerType.Name.Length - DefaultHttpControllerSelector.ControllerSuffix.Length);

                var controllerKey = string.Format(CultureInfo.InvariantCulture, "{0}.{1}", segments[segments.Length - 1], controllerName);

                if (!dictionary.Keys.Contains(controllerKey))
                {
                    dictionary[controllerKey] = new HttpControllerDescriptor(_configuration, controllerType.Name, controllerType);
                }
            }

            return dictionary;
        }

        private string GetControllerName(IHttpRouteData routeData)
        {
            var subroute = routeData.GetSubRoutes().FirstOrDefault();
            if (subroute == null) return null;

            var dataTokenValue = subroute.Route.DataTokens.First().Value;
            if (dataTokenValue == null) return null;

            var controllerName = ((HttpActionDescriptor[])dataTokenValue).First().ControllerDescriptor.ControllerName.Replace("Controller", string.Empty);
            return controllerName;
        }

        private string GetVersion(IHttpRouteData routeData)
        {
            var subRouteData = routeData.GetSubRoutes().FirstOrDefault();

            if (subRouteData == null) return null;


            // Modified to allow for . versioning, eg v1.1 etc

            var apiVersion = GetRouteVariable<string>(subRouteData, "apiVersion");

            var apiVersionNamespace = apiVersion.Replace('.', '_');

            return apiVersionNamespace;
        }

        private T GetRouteVariable<T>(IHttpRouteData routeData, string name)
        {
            object result;
            if (routeData.Values.TryGetValue(name, out result))
            {
                return (T)result;
            }
            return default(T);
        }
    }

Here is the HttpConfiguration :-

public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {


            var constraintsResolver = new DefaultInlineConstraintResolver();

            constraintsResolver.ConstraintMap.Add("apiVersionConstraint", typeof(ApiVersionConstraint));

            config.MapHttpAttributeRoutes(constraintsResolver);

            config.Services.Replace(typeof(IHttpControllerSelector), new NamespaceHttpControllerSelector(config));



        }
    }

And Finally, here is your Controller :-

[ApiVersion1RoutePrefix("products")]
public class ProductsController : ApiController {
    Product[] products = new Product[] {
        new Product { UserReference = "a", Name = "product1" },
        new Product { UserReference = "b", Name = "product2" }
    };

    [HttpGet]
    [Route("")]
    public IEnumerable<Product> GetAllProducts() {
        return products;
    }

    [HttpGet]
    [Route("{userReference}")]
    public IHttpActionResult GetProducts(string userReference) {
        var res = products.Where(t => t.UserReference == userReference);

        if (res == null)
            return NotFound();

        return Ok(res);
    }
}

When you want to use v2 you just use the

[ApiVersion2RoutePrefix("products")]

Upvotes: 1

Related Questions