Reputation:
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
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
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