Why versioning required in Web API?- Once a Web API service is made public, different client applications start using Web API services.
- As the requirements change, we may have to change the services as well, but the changes to the services should be done in way that does not break any existing client applications.
- This is when Web API versioning helps. We keep the existing services as is, so we are not breaking the existing client applications, and develop a new version of the service that new client applications can start using.
Different options available to version Web API services :
- URI's
- Query String
- Version Header
- Accept Header
- Media Type
1. URI's
Using Convention Based Routing- We can have multiple routes in WebApiConfig.cs. One route for one version.
config.Routes.MapHttpRoute(
name: "Version1",
routeTemplate: "api/v1/Students/{id}",
defaults: new { id = RouteParameter.Optional, controller = "StudentsV1" }
);
config.Routes.MapHttpRoute(
name: "Version2",
routeTemplate: "api/v2/Students/{id}",
defaults: new { id = RouteParameter.Optional, controller = "StudentsV2" }
);
Using Attribute Routing:-
Use the [Route] attribute on methods in StudentsV1Controller and StudentsV2Controller as shown below.
public class StudentsV1Controller : ApiController
{
[Route("api/v1/students")]
public IEnumerable<StudentV1> Get() {...}
[Route("api/v1/students/{id}")]
public StudentV1 Get(int id) {...}
}
public class StudentsV2Controller : ApiController
{
[Route("api/v2/students")]
public IEnumerable<StudentV2> Get() {...}
[Route("api/v2/students/{id}")]
public StudentV2 Get(int id) {...}
}
2. Query String
For this we need to have our own custom controller selector.Steps to version Web API service using a query string parameter as follows,
Here is what we want
URI | Should Return |
---|---|
/api/students?v=1 | Version 1 Students |
/api/students?v=2 | Version 2 Students |
Step 1 : Since the default controller selector implementation provided by Web API does not work for us, we have to provide our own custom controller selector implementation. To do this
1. Add a folder to the web api project. Name it "Custom"
2. Add a class file to the folder. Name it "CustomControllerSelector". Copy and paste the following code. it is self explanatory
using System.Net.Http;
using System.Web;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.Dispatcher;
namespace WebAPI.Custom
{
// Derive from the DefaultHttpControllerSelector class
public class CustomControllerSelector : DefaultHttpControllerSelector
{
private HttpConfiguration _config;
public CustomControllerSelector(HttpConfiguration config) : base(config)
{
_config = config;
}
public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
{
// Get all the available Web API controllers
var controllers = GetControllerMapping();
// Get the controller name and parameter values from the request URI
var routeData = request.GetRouteData();
// Get the controller name from route data.
// The name of the controller in our case is "Students"
var controllerName = routeData.Values["controller"].ToString();
// Default version number to 1
string versionNumber = "1";
var versionQueryString = HttpUtility.ParseQueryString(request.RequestUri.Query);
if (versionQueryString["v"] != null)
{
versionNumber = versionQueryString["v"];
}
if (versionNumber == "1")
{
// if version number is 1, then append V1 to the controller name.
// So at this point the, controller name will become StudentsV1
controllerName = controllerName + "V1";
}
else
{
// if version number is 2, then append V2 to the controller name.
// So at this point the, controller name will become StudentsV2
controllerName = controllerName + "V2";
}
HttpControllerDescriptor controllerDescriptor;
if (controllers.TryGetValue(controllerName, out controllerDescriptor))
{
return controllerDescriptor;
}
return null;
}
}
}
Step 2 : The next thing that we need to do is, replace the default controller selector with our custom controller selector. This is done in WebApiConfig.cs file. Notice we are replacing IHttpControllerSelector, with our CustomControllerSelector. DefaultHttpControllerSelector implements IHttpControllerSelector, so that is the reason we are replacing IHttpControllerSelector.
config.Services.Replace(typeof(IHttpControllerSelector),
new CustomControllerSelector(config));
Step 3 : Include the following default route in WebApiConfig.cs
config.Routes.MapHttpRoute(
name: "DefaultRoute",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
Step 4 : Remove [Route] attribute, from action methods
3. Version Header
To implement versioning using a custom version header, we have to change the logic slightly int the CustomControllerSelector class to read the version number from the custom version header instead of from a query string parameter.
As showin in follwing code, we can get vesionNumber from header as follows,
string customHeader = "X-StudentService-Version";
if (request.Headers.Contains(customHeader))
{
versionNumber = request.Headers.GetValues(customHeader).FirstOrDefault();
}
4. Accept header
The Accept header tells the server in what file format the browser wants the data. These file formats are more commonly called as MIME-types. MIME stands for Multipurpose Internet Mail Extensions.
Instead of creating a custom header, we can use the standard Accept header. We can add parameters to the Accept header to send any additional data along with the request to the server.
For example, we can specify the version of the service we want using the version parameter.
Now, we need to read the version parameter value from the Accept header in our CustomControllerSelector class.
// Check if any of the Accept headers has a parameter with name version
var acceptHeader = request.Headers.Accept.Where(a => a.Parameters
.Count(p => p.Name.ToLower() == "version") > 0);
// If there is atleast one header with a "version" parameter
if (acceptHeader.Any())
{
// Get the version parameter value from the Accept header
versionNumber = acceptHeader.First().Parameters
.First(p => p.Name.ToLower() == "version").Value;
}
5. Media Type
Instead of using the standard media types like application/xml or application/json, we can use custom media type.
In the media type, we have the version of the service. Custom media types have vnd prefix. vnd indicates that it is a vendor specific media type.
So from our CustomControllerSelector class we will read the version number from the custom media type we have using regex.
// Get the version number from the Custom media type
// Use regular expression for mataching the pattern of the media type
string regex =
@"application\/vnd\.mangaltech\.([a-z]+)\.v(?<version>[0-9]+)\+([a-z]+)";
// Users can include multiple Accept headers in the request.
// Check if any of the Accept headers has our custom media type by
// checking if there is a match with regular expression specified
var acceptHeader = request.Headers.Accept
.Where(a => Regex.IsMatch(a.MediaType, regex, RegexOptions.IgnoreCase));
// If there is atleast one Accept header with our custom media type
if (acceptHeader.Any())
{
// Retrieve the first custom media type
var match = Regex.Match(acceptHeader.First().MediaType,
regex, RegexOptions.IgnoreCase);
// From the version group, get the version number
versionNumber = match.Groups["version"].Value;
}
Superb
ReplyDelete