API versioning in. NET MVC 4

mercredi 23 janvier 2013

With the advent of ASP.NET Web API has a new and powerful tool for generating API to your site. But as you know, over time, your API may change, may be amended or completely rewritten from scratch. For compatibility with old clients need to implement versioning.

Unfortunately, at this point Microsoft does not provide a convenient and easy way to implement versioning. On the Internet you can find some information on this subject, but as a rule, the majority found my solution was to add a parameter to the versions in each query and its processing. I also wanted a more flexible method for sharing on a version that will not clog the controller methods and eliminate the multiple blocks if else. And the most important criteria for me was the opportunity to have a controller with the same name to the same methods of API, but separated by a version with namespaces.

At the same time, in ASP.NET MVC Web API is quite powerful mechanism in the form of interface IHttpControllerSelector , 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 number that would be interpreted as a version of the parameter (in kontollerah we'll just ignore).

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

Thus all of our API methods have the form api / v {version} / {controller} / {id}, where {version} - version API. In fact, you can use not only numbers, but in general you want. The main thing is that we can in this parameter to distinguish the implementation API.

Next, you need to establish the correct handling of queries: the choice and construction of controllers. This process looks very simple. Factory controllers must know which controller it must be created. That's what is the interface IHttpControllerSelector.

In most cases, we are fully satisfied with the standard DefaultHttpControllerSelector , therefore, to implement versioning is not necessary to write it entirely from scratch.

For this, we otnasludemsya from DefaultHttpControllerSelector and override its main method SelectController. It is responsible for the selection of the controller and provides a handle to the controller 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 HttpConfiguration current as a parameter. Therefore register in IoC container with his addiction. Most often I use Castle Windsor , so in the example used by him.

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

Now let's move on to the process of selecting the appropriate controller method SelecController.
Controller factory expects us deskiptor HttpControllerDescriptor , which consists of the name of the controller and its type.

 HttpControllerDescriptor(HttpConfiguration, String, Type) 

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

 var controllerName = GetControllerName(request); 

If the name is simple and clear, to determine the type of controller, we need to know the controllers present in our system. We add a field and a method for their calculation. After that, our class is as follows:

 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); } } 

Also, we need the version number of the request

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

Add a method to get the type of the controller according to

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

The method itself is very simple: from a list of types of controllers, we need to select a controller, which lies in the corresponding version of API neymspeyse

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

There are two custom controllers to filter eksteshnena

 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 you need to create a handle of the controller. Resulting 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)); } } 

The result is a simple mechanism for versioning API with the ability to have a view controller

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


If your API has not changed fundamentally, we need to fix a few methods of filtration, which would choose the controller latest version.

Thank you for your attention.

0 commentaires:

Enregistrer un commentaire

 
© Copyright 2010-2011 GARMOBI All Rights Reserved.
Template Design by Herdiansyah Hamzah | Published by Borneo Templates | Powered by Blogger.com.