📜 ⬆️ ⬇️

Versioning API in .NET MVC 4

Good day.

With the advent of ASP.NET Web API, a convenient and powerful tool for creating an API for your site has appeared. But, as you know, over time, your API can change, be supplemented, or can be completely redone from scratch. For compatibility with old clients it is necessary to implement versioning.

Unfortunately, at the moment Microsoft has not provided a convenient and easy way to implement versioning. On the Internet, you can find some information on this topic, but, as a rule, most of the solutions I found come down to adding a parameter for a version to each request and processing it. I also wanted to get a more flexible method for versioning, which will not clutter up the controller methods and eliminate the set of if else blocks. And the most important criterion for me was the ability to have controllers with the same names for the same API methods, but divided into versions using namespaces.
')
At the same time, in ASP.NET MVC Web API there is a rather powerful mechanism in the form of the IHttpControllerSelector interface, with which you can implement versioning, leaving the code clean and clear.

Let's see what came of it.


First of all, we need to properly configure the routing so that the version number is interpreted as a parameter (in the controllers, we will simply ignore it).

httpRoutes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/v{version}/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); 

Thus, all our API methods will be of the form api / v {version} / {controller} / {id}, where {version} is the version number of the API. In fact, you can use not only numbers, but anything else. The main thing is that we can distinguish API implementations by this parameter.

Next, you need to adjust the correct processing of requests: the selection and creation of controllers. This process looks very simple. The controller factory must know which controller it needs to create. This is exactly what the IHttpControllerSelector interface serves as.

In most cases, we are completely satisfied with the standard DefaultHttpControllerSelector , so in order to implement versioning it is not necessary to completely write it from scratch.

To do this, we’ll remove the DefaultHttpControllerSelector and override its main SelectController method. It is he who is responsible for selecting the controller and provides the controller with a handle to the factory.

 public class HttpControllerSelector : DefaultHttpControllerSelector { private readonly HttpConfiguration configuration; public HttpControllerSelector(HttpConfiguration configuration) : base(configuration) { this.configuration = configuration; } public override HttpControllerDescriptor SelectController(HttpRequestMessage request) { } } 

ControllerSelector requires the current HttpConfiguration as a parameter. Therefore, we register it in the IoC container with a dependency. Most often I use Castle Windsor , so the example uses it.

 container.Register( Component.For<IHttpControllerSelector>().ImplementedBy<HttpControllerSelector>().DependsOn( Dependency.OnValue<HttpConfiguration>(GlobalConfiguration.Configuration))); 

Now we proceed directly to the process of selecting the appropriate controller in the SelecController method.
The controller factory expects the HttpControllerDescriptor descriptor from us, which consists of the controller name and its type.

 HttpControllerDescriptor(HttpConfiguration, String, Type) 

To get the name of the controller, we can use the basic functionality of the DefaultHttpControllerSelector class.

 var controllerName = GetControllerName(request); 

If the name is simple and clear, then to determine the type of controller, we need to know the controllers that are in our system. To do this, add a field and a method to calculate them. After this, our class looks like this:

 public class HttpControllerSelector : DefaultHttpControllerSelector { private readonly HttpConfiguration configuration; private readonly Lazy<ConcurrentDictionary<string, Type>> controllerTypes; public HttpControllerSelector(HttpConfiguration configuration) : base(configuration) { this.configuration = configuration; controllerTypes = new Lazy<ConcurrentDictionary<string, Type>>(GetControllerTypes); } public override HttpControllerDescriptor SelectController(HttpRequestMessage request) { var controllerName = GetControllerName(request); } private static ConcurrentDictionary<string, Type> GetControllerTypes() { var assemblies = AppDomain.CurrentDomain.GetAssemblies(); var types = assemblies .SelectMany(a => a.GetTypes().Where(t => !t.IsAbstract && t.Name.EndsWith(ControllerSuffix, StringComparison.OrdinalIgnoreCase) && typeof (IHttpController).IsAssignableFrom(t))) .ToDictionary(t => t.FullName, t => t); return new ConcurrentDictionary<string, Type>(types); } } 

We also need the version number from the request.

 object version; request.GetRouteData().Values.TryGetValue("version", out version); 

Add a method to get the type of controller version

 var type = GetControllerType((string)version, controllerName); 

The method itself is extremely simple: from the list of controller types we need to select a controller that lies in the namespace of the corresponding API version.

 private Type GetControllerType(string version, string controllerName) { var query = controllerTypes.Value.AsEnumerable(); return query.ByVersion(version) .ByControllerName(controllerName) .Select(x => x.Value) .Single(); } 

It uses two custom extensions to filter the controllers.

 public static IEnumerable<KeyValuePair<string, Type>> ByVersion(this IEnumerable<KeyValuePair<string, Type>> query, string version) { var versionNamespace = string.Format(CultureInfo.InvariantCulture, ".V{0}.", version); return query.Where(x => x.Key.IndexOf(versionNamespace, StringComparison.OrdinalIgnoreCase) != -1); } public static IEnumerable<KeyValuePair<string, Type>> ByControllerName(this IEnumerable<KeyValuePair<string, Type>> query, string controllerName) { var controllerNameToFind = string.Format(CultureInfo.InvariantCulture, ".{0}{1}", controllerName, DefaultHttpControllerSelector.ControllerSuffix); return query.Where(x => x.Key.EndsWith(controllerNameToFind, StringComparison.OrdinalIgnoreCase)); } 

Now we have everything we need to create a controller handle. Result class:

 public class HttpControllerSelector : DefaultHttpControllerSelector { private readonly HttpConfiguration configuration; private readonly Lazy<ConcurrentDictionary<string, Type>> controllerTypes; public HttpControllerSelector(HttpConfiguration configuration) : base(configuration) { this.configuration = configuration; controllerTypes = new Lazy<ConcurrentDictionary<string, Type>>(GetControllerTypes); } public override HttpControllerDescriptor SelectController(HttpRequestMessage request) { object version; request.GetRouteData().Values.TryGetValue("version", out version); var controllerName = GetControllerName(request); var type = GetControllerType((string)version, controllerName); return new HttpControllerDescriptor(configuration, controllerName, type); } private static ConcurrentDictionary<string, Type> GetControllerTypes() { var assemblies = AppDomain.CurrentDomain.GetAssemblies(); var types = assemblies .SelectMany(a => a.GetTypes().Where(t => !t.IsAbstract && t.Name.EndsWith(ControllerSuffix, StringComparison.OrdinalIgnoreCase) && typeof (IHttpController).IsAssignableFrom(t))) .ToDictionary(t => t.FullName, t => t); return new ConcurrentDictionary<string, Type>(types); } private Type GetControllerType(string version, string controllerName) { var query = controllerTypes.Value.AsEnumerable(); return query.ByVersion(version) .ByControllerName(controllerName) .Select(x => x.Value) .Single(); } } public static class ControllerTypeSpecifications { public static IEnumerable<KeyValuePair<string, Type>> ByVersion(this IEnumerable<KeyValuePair<string, Type>> query, string version) { var versionNamespace = string.Format(CultureInfo.InvariantCulture, ".V{0}.", version); return query.Where(x => x.Key.IndexOf(versionNamespace, StringComparison.OrdinalIgnoreCase) != -1); } public static IEnumerable<KeyValuePair<string, Type>> ByControllerName(this IEnumerable<KeyValuePair<string, Type>> query, string controllerName) { var controllerNameToFind = string.Format(CultureInfo.InvariantCulture, ".{0}{1}", controllerName, DefaultHttpControllerSelector.ControllerSuffix); return query.Where(x => x.Key.EndsWith(controllerNameToFind, StringComparison.OrdinalIgnoreCase)); } } 

As a result, we have a simple API versioning mechanism with the ability to have controllers of the form

 Controllers.Api.V1.UserController Controllers.Api.V2.UserController 


If your API has not changed radically, then we only need to slightly correct the filtering methods that would select the controller of the latest available version.

Thanks for attention.

Source: https://habr.com/ru/post/165149/


All Articles