ASP.NET Web API is a framework to build HTTP services for multiple clients, including browsers and mobile devices. ASP.NET Web API is platform to build RESTful applications on top of .NET Framework. If creating ASP.NET Web API for multiple clients then it could be required to have versioning in API which allows you to alter behavior between different clients.
Why need versioning?
- API versioning should be required if any change in data contract such as renaming or deleting parameter or change in format of the response is required.
- Change in URL can also be considered for versioning. E.g. when it changes from api\user?id=123 to api\user\123 can be considered change in version.
- API versioning will introduce complexity for both service implementation and client using API.
REST does not provide any versioning but it can be achieved with help of one of the following more commonly used approaches,
URI Path
-
- Add API version in URI. e.g. api/v1/user/123 and api/v2/user/123
- It is very common and most straightforward approach though it violets REST API design who insist a URI should refer to unique resource.
- Custom Request Header
- In this approach, customer header allows you to use URIs with versions which will be duplicate of content negotiation behavior implemented by existing Accept header. e.g. api-version: 2
- Content Negotiation
- It most recommended approach nowadays, it allows clean set of URIs with help of accept header with some complexity to server different version of content/resource. e.g. Accept: application/vnd.domain.v2+json
API versioning, which to choose?
- Considering the continuous change in API, any versioning approach could be wrong at some point.
- A real challenge with versioning is managing the code base which serving multiple versions of resource. If all the versions are kept in same code base then older versions could be vulnerable at some point for unexpected changes. If code base are different then very high chances of escalation in maintenance.
- A versioning could be obstacle to improvements as version change are restricted.
- Versioning with content negotiation and custom headers are popular nowadays, but versioning with URL are more common now as it's easier to implement.
- It’s a pragmatic decision, but API should have version from the first release.
Resource versioning?
- With versioning of API it should be required to version resources as different version may return resource with change. It can be easily achieved with help of inheritance.
ASP.NET Web API versioning can be achieved by extending DefaultHttpControllerSelector, following code illustrates versioning with content negotiation
public class ContentNegotiationVersioningSelector : DefaultHttpControllerSelector
{
private HttpConfiguration _HttpConfiguration;
public ContentNegotiationVersioningSelector(HttpConfiguration httpConfiguration) : base(httpConfiguration)
{
_HttpConfiguration = httpConfiguration;
}
/// <summary>
/// extesnsion to default controller selector
/// </summary>
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
HttpControllerDescriptor controllerDescriptor = null;
// get list of controllers provided by the default selector
IDictionary<string, HttpControllerDescriptor> controllers = GetControllerMapping();
// get request route data
IHttpRouteData routeData = request.GetRouteData();
if (routeData == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
// get api version from accept header
var apiVersion = GetVersion(request);
//check if this route is actually an attribute route
IEnumerable<IHttpRouteData> attributeRoutes = routeData.GetSubRoutes();
if (attributeRoutes == null)
{
string controllerName = GetRouteVariable<string>(routeData, "controller");
if (controllerName == null)
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
string versionControllerName = String.Concat(controllerName, "V", apiVersion);
if (controllers.TryGetValue(versionControllerName, out controllerDescriptor))
{
return controllerDescriptor;
}
else
{
throw new HttpResponseException(HttpStatusCode.NotFound);
}
}
else
{
// find all controller descriptors whose controller type names end with
// the following suffix (example: ControllerV1)
string versionControllerName = String.Concat("V", apiVersion);
IEnumerable<IHttpRouteData> filteredAttributeRoutes = attributeRoutes
.Where(attrRouteData =>
{
HttpControllerDescriptor currentDescriptor = GetControllerDescriptor(attrRouteData);
bool match = currentDescriptor.ControllerName.EndsWith(versionControllerName);
if (match && (controllerDescriptor == null))
{
controllerDescriptor = currentDescriptor;
}
return match;
});
routeData.Values["MS_SubRoutes"] = filteredAttributeRoutes.ToArray();
}
return controllerDescriptor;
}
/// <summary>
/// gets the controller descriptor for attribute routes.
/// </summary>
private HttpControllerDescriptor GetControllerDescriptor(IHttpRouteData routeData)
{
return ((HttpActionDescriptor[])routeData.Route.DataTokens["actions"]).First().ControllerDescriptor;
}
/// <summary>
/// gets value from the route data, if present.
/// </summary>
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);
}
/// <summary>
/// gets the type of the version
/// Accept: application/vnd.domain.v{version}+json
/// </summary>
private string GetVersion(HttpRequestMessage request)
{
var acceptHeader = request.Headers.Accept;
var regex = new Regex(@"application\/vnd\.domain\.v([\d]+)\+json", RegexOptions.IgnoreCase);
foreach (var mime in acceptHeader)
{
Match match = regex.Match(mime.MediaType);
if (match.Success == true)
{
return match.Groups[1].Value; // change group selection based on regex if requried
}
}
return "1"; // return latest version if not accept header provided (should be configured)
}
}
Add following line to replace default controller selector,
config.Services.Replace(typeof(IHttpControllerSelector), new ContentNegotiationVersioningSelector(config));
Controller hierarchy for versioning
Versioning resource or POCO classes
Similarly, URL and custom header based versioning can be implemented by extending DefaultHttpControllerSelector as above.