How to include API version number v1, v2, v3, etc in the path (like .../v1/...
) and have it:
Here is how HTTP calls to different API versions would look like:
/v1/User/SomeAction?param=zzz
/v2/User/SomeAction?param=zzz
....
/vNN/User/SomeAction?param=zzz
Now let's take a look at the server side. In most scenarios it would be convenient to have 3 controller classes with the same name UserController but in different namespaces:
namespace Api.Controllers.V1
{
// V1 controller
public class UserController : ApiController { }
}
namespace Api.Controllers.V2
{
// V2 controller, inherits from V1 controller
public class UserController : Api.Controllers.V1.UserController { }
}
namespace Api.Controllers.V3
{
// V3 controller, inherits from V2 controller
public class UserController : Api.Controllers.V2.UserController { }
}
Note that here I've also made V3 inherit from V2 and V2 inherit from V1. This may not necessarily be the best design choice. It may depend on the following:
Regardless of your choice, Web API routing problems would remain the same.
The problem with this approach is that Web API locates controllers by class name only, ignoring the namespace. So there’s no way to make this work using the default routing logic.
Question - how to set up routing so that the value of "vNN" part from the HTTP would be used to select correct controller class? Thanks!
You may need to program your custom routing logic by implementing IHttpControllerSelector
interface to change default routing behavior. Method SelectController
is used to select a controller for a given request - HttpRequestMessage
. You would need to return type
object for the controller type you want to be used.
type
object into static Dictionary cache variable using special key crafted from full type name's segments to extract preceding namespace part and form a string like this v1.user
register route in config:
// Register default route
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "{namespace}/{controller}/{action}"
);
inject your custom class CustomControllerSelector
into routing config:
config.Services.Replace(typeof(System.Web.Http.Dispatcher.IHttpControllerSelector), new CustomControllerSelector(config)); ...
the injected CustomControllerSelector.SelectController(HttpRequestMessage request)
would get called for you automatically, and from request
parameter variable you would need to build that Key = v1.user
and use it in that static Dictionary to look up cached controller type object that corresponds to that Key. Done!
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Dispatcher;
using System.Web.Http.Routing;
public class CustomControllerSelector : IHttpControllerSelector
{
private const string NamespaceKey = "namespace";
private const string ControllerKey = "controller";
private readonly HttpConfiguration _configuration;
private readonly Lazy<Dictionary<string, HttpControllerDescriptor>> _controllers;
private readonly HashSet<string> _duplicates;
public CustomControllerSelector(HttpConfiguration config)
{
_configuration = config;
_duplicates = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
_controllers = new Lazy<Dictionary<string, HttpControllerDescriptor>>(InitializeControllerDictionary);
}
private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary()
{
var dictionary = new Dictionary<string, HttpControllerDescriptor>(StringComparer.OrdinalIgnoreCase);
// http routing: '/v1/User/SomeAction' => Controllers.V1.UserController
//
// Create a lookup table where key is "namespace.controller", i.e. "V1.User" or "V2.User"
// Controllers.V1.UserController => "V1.User"
//
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;
}
// Get a value from the route data, if present.
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);
}
public HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
IHttpRouteData routeData = request.GetRouteData();
if (routeData == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
// Get the namespace and controller variables from the route data.
string namespaceName = GetRouteVariable<string>(routeData, NamespaceKey);
if (namespaceName == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
string controllerName = GetRouteVariable<string>(routeData, ControllerKey);
if (controllerName == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
// Find a matching controller.
string key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", namespaceName, 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;
}
}
If you created Web API project from standard template, then this class should be already in your project. Its method Register
is called ONCE on application start. So just make sure you have the following routing config code in there:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
// Register default route
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "{namespace}/{controller}/{action}"
);
config.Services.Replace(typeof(System.Web.Http.Dispatcher.IHttpControllerSelector), new CustomControllerSelector(config));
}
}
...
FavScripts.com is a free tool to save your favorite scripts and commands, then quickly find and copy-paste your commands with just few clicks.
Boost your productivity with FavScripts.com!