Web API 版本化的介绍

来源:互联网 发布:类似漂流瓶的软件 编辑:程序博客网 时间:2024/05/10 06:51

Web API 版本化的介绍

返回原文英文原文:Introduction to Web API Versioning

  • WebAPI HTTP Custom Header Based Versioning

  • WebAPI Namespace Based Versioning

Introduction

In this article lets discuss about Web API versioning and the need for the same. 

Versioning of API is very much required if you are providing services for the public. It's the only way we can make our API's supporting forward and backward compatibility. By versioning you don't effect the existing customers and break their applications which depends on your API.

The default Web API routing logic finds the controller by class name. The controller selection is handled internally by Web API through DefaultHttpControllerSelector which implementsIHttpControllerSelector.SelectController. We can't really version Web API's with this default controller selector but we can plug in our custom implementation for selecting the right version of our controller at run time. By implementing IHttpControllerSelector interface one can easily create a custom HTTP controller selector.

Let us see two different kinds of Web API versioning. You can decide on which ever is best and suits your needs. One can either go with namespace or HTTP custom header based versioning mechanism.

译者信息
  • 基于自定义HTTP头的Web API版本化

  • 基于命名空间的Web API版本化

介绍

本文中,我们来讨论一下Web API版本化的问题,以及同类需求。 

当你准备提供公共服务接口时,Web API的版本化问题会显得非常必要。它是我们为API提供向前、向后兼容支持的唯一途径。通过版本化,你能够不影响现有用户的使用,而且不会破坏那些依赖于你提供的API的应用。

默认的Web API的路由逻辑是:通过类名来查找controller。Controller的选取则是在Web API内部通过选择器 DefaultHttpControllerSelector(它实现了 IhttpControllerSelector.SelectController)来处理。我们无法通过默认的controller选择器实现Web API的版本化,但却可以在运行时通过自定义插件实现正确的版本选择。实现 IhttpControllerSelector接口就可以轻松创建自定义的HTTP controller选择器。

来看看两种不同的Web API版本化方式,你可以自己决定哪一个是符合需求的最佳方式。一个是利用命名空间,一个是基于自定义HTTP头部信息的机制。

Background

Knowledgeable and have good understanding and development experience in Web API.

Using the code

Below is the code snippet for getting the version number from HTTP custom header. Let us assume the default version of our Web API will always be empty.

private string GetVersionFromHTTPHeader(HttpRequestMessage request){      if (request.Headers.Contains("version"))      {          var versionHeader = request.Headers.GetValues("version").FirstOrDefault();          if (versionHeader != null)          {               return versionHeader;          }      }      return string.Empty;}

Below is the code snippet to get the version number based on the Mime Type. You can use either of the code to get the version number.

private string GetVersionFromAcceptHeaderVersion(HttpRequestMessage request) {       var acceptHeader = request.Headers.Accept;       foreach (var mime in acceptHeader)       {             if (mime.MediaType == "application/json" || mime.MediaType == "text/html")             {                  var version = mime.Parameters                                   .Where(v => v.Name.Equals("version", StringComparison.OrdinalIgnoreCase))                                    .FirstOrDefault();                   if (version != null)                   {                       return version.Value;                   }                   return string.Empty;             }       }       return string.Empty; }

Below is the code snippet for custom Web API controller selector which implements IHttpControllerSelector.SelectController. From the below code you can notice that we are returning the controller descriptor based on the matched controller mappings or by version.

public override HttpControllerDescriptor SelectController(HttpRequestMessage request){        var controllers = GetControllerMapping();         var routeData = request.GetRouteData();        var controllerName = routeData.Values["controller"].ToString();        HttpControllerDescriptor controllerDescriptor;        if (controllers.TryGetValue(controllerName, out controllerDescriptor))        {                var version = GetVersionFromHTTPHeader(request);                if (!string.IsNullOrEmpty(version))                {                    var versionedControllerName = string.Concat(controllerName, "V", version);                    HttpControllerDescriptor versionedControllerDescriptor;                    if (controllers.TryGetValue(versionedControllerName, out versionedControllerDescriptor))                    {                        return versionedControllerDescriptor;                    }                }                return controllerDescriptor;        }        return null;}

译者信息

背景

本人在Web API方面知识渊博、有很好的理解和开发经验。

代码示例

下面的代码片段是为了得到HTTP自定义头部的版本号。在这我们设定Web API的默认版本号总是空的。

private string GetVersionFromHTTPHeader(HttpRequestMessage request){      if (request.Headers.Contains("version"))      {          var versionHeader = request.Headers.GetValues("version").FirstOrDefault();          if (versionHeader != null)          {               return versionHeader;          }      }      return string.Empty;}

下面的代码片段用于获取基于MIME类型的版本号。你能任选其一来获取版本号。

private string GetVersionFromAcceptHeaderVersion(HttpRequestMessage request) {       var acceptHeader = request.Headers.Accept;       foreach (var mime in acceptHeader)       {             if (mime.MediaType == "application/json" || mime.MediaType == "text/html")             {                  var version = mime.Parameters                                   .Where(v => v.Name.Equals("version", StringComparison.OrdinalIgnoreCase))                                    .FirstOrDefault();                   if (version != null)                   {                       return version.Value;                   }                   return string.Empty;             }       }       return string.Empty; }

下面的代码片段是自定义Web API 控制选择器,它实现了IHttpControllerSelector.SelectController函数。从下面的代码中你能注意到我们会基于匹配到的控制映射或者通过版本来返回控制器描述信息。

public override HttpControllerDescriptor SelectController(HttpRequestMessage request){        var controllers = GetControllerMapping();         var routeData = request.GetRouteData();        var controllerName = routeData.Values["controller"].ToString();        HttpControllerDescriptor controllerDescriptor;        if (controllers.TryGetValue(controllerName, out controllerDescriptor))        {                var version = GetVersionFromHTTPHeader(request);                if (!string.IsNullOrEmpty(version))                {                    var versionedControllerName = string.Concat(controllerName, "V", version);                    HttpControllerDescriptor versionedControllerDescriptor;                    if (controllers.TryGetValue(versionedControllerName, out versionedControllerDescriptor))                    {                        return versionedControllerDescriptor;                    }                }                return controllerDescriptor;        }        return null;}

So what next ? 

Now that we have implemented custom controller selector. We will have to just replace the default one and make use of our custom WebAPI controller selector. This can be easily done by adding one line of code in WebApiConfig.

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/{controller}/{id}",                defaults: new { id = RouteParameter.Optional }            );            // Replace the controller configuration selector            config.Services.Replace(typeof(IHttpControllerSelector), new CustomControllerSelector((config)));        }}


Below is the snapshot of the HTTP Request and Response. You can notice the "version" is being passed in HTTP Request header.






We shall see in brief about namespace based Web API versioning. This would be one of the most common technique one would make use and you will see most of the popular service providers do make use of these kind of techniques.

译者信息

那么接下来呢?

上面我们已经实现了自定义的控制器选择器,现在必须让它替换默认的以使生效。只要在 WebApiConfig 中添加一行就行。

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/{controller}/{id}",                defaults: new { id = RouteParameter.Optional }            );            // 在这替换控制器选择器            config.Services.Replace(typeof(IHttpControllerSelector), new CustomControllerSelector((config)));        }}

以下是HTTP请求和响应的快照。你可以注意到在HTTP请求头中被传递的版本信息“version”。

我们应该看到过有关基于命名空间的 Web API 版本控制的简介。这是一种最常见的被用到的技术,你会看到最流行的服务提供商也在使用这种技术。

Below is the code snippet which essentially acts as a look up by returning the controller descriptor based on the namespace.

private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary(){            var dictionary = new Dictionary<string,>(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;}

Below is the code snippet for namespace based custom controller selector.

We will be getting the namespace and controller from the HTTP Request Message and then we will be finding the matching controller with in the dictionary. In order to find the match we will build the matching key as <namespace name>:<controller name>

public HttpControllerDescriptor SelectController(HttpRequestMessage request){            IHttpRouteData routeData = request.GetRouteData();            if (routeData == null)            {                throw new HttpResponseException(HttpStatusCode.NotFound);            }            // Get the namespace and controller variables from the route data.            string namespaceName = GetRouteVariable<string>(routeData, “namespace”);            if (namespaceName == null)            {                throw new HttpResponseException(HttpStatusCode.NotFound);            }            string controllerName = GetRouteVariable<string>(routeData, “controller”);            if (controllerName == null)            {                throw new HttpResponseException(HttpStatusCode.NotFound);            }            // Find a matching controller.            string key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", namespaceName, 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);            }}// Get a value from the route data, if present.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);}

译者信息

下面的代码片段通过返回基于命名空间的控制器描述符来实现查找功能。

private Dictionary<string, HttpControllerDescriptor> InitializeControllerDictionary(){            var dictionary = new Dictionary<string,>(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;}

下面的代码片段对应于自定义控制选择器的命名空间。我们将从TTP请求消息中获取命名空间和控制器,然后在字典中查找匹配的控制器。为了匹配对应关系我们构建了匹配映射:<namespace name>:<controller name>

public HttpControllerDescriptor SelectController(HttpRequestMessage request){            IHttpRouteData routeData = request.GetRouteData();            if (routeData == null)            {                throw new HttpResponseException(HttpStatusCode.NotFound);            }            // Get the namespace and controller variables from the route data.            string namespaceName = GetRouteVariable<string>(routeData, “namespace”);            if (namespaceName == null)            {                throw new HttpResponseException(HttpStatusCode.NotFound);            }            string controllerName = GetRouteVariable<string>(routeData, “controller”);            if (controllerName == null)            {                throw new HttpResponseException(HttpStatusCode.NotFound);            }            // Find a matching controller.            string key = String.Format(CultureInfo.InvariantCulture, "{0}.{1}", namespaceName, 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);            }}// Get a value from the route data, if present.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);}

Below is the code snippet on the usage of namespace based WebAPI Versioning. You can notice the HTTP Request has the version and controller names specified. 

static void RunClient(){      HttpClient client = new HttpClient();      client.BaseAddress = _baseAddress;      using (HttpResponseMessage response = client.GetAsync("api/v1/values").Result)      {          response.EnsureSuccessStatusCode();          string content = response.Content.ReadAsStringAsync().Result;          Console.WriteLine("Version 1 response: &apos;{0}&apos;\n", content);      }      using (HttpResponseMessage response = client.GetAsync("api/v2/values").Result)      {          response.EnsureSuccessStatusCode();          string content = response.Content.ReadAsStringAsync().Result;          Console.WriteLine("Version 2 response: &apos;{0}&apos;\n", content);      }}

References

http://blogs.msdn.com/b/webdev/archive/2013/03/08/using-namespaces-to-version-web-apis.aspx

https://mathieu.fenniak.net/aint-nobody-got-time-for-that-api-versioning/

http://aspnet.codeplex.com/SourceControl/changeset/view/dd207952fa86#Samples/WebApi/NamespaceControllerSelector/

http://bitoftech.net/2013/12/16/asp-net-web-api-versioning-strategy/

Points of Interest

Versioning of Web API for me it's a required thing in what ever service that I can think of providing it to the public. It was really a fun in implementing Web API versioning. I really love the plug and play or replacement of HTTP Controller Selector. 

译者信息

下面是利用基于名称空间的 WebAPI 版本控制的代码片段。你可以注意到HTTP请求带有指定的版本和控制器名称。

static void RunClient(){      HttpClient client = new HttpClient();      client.BaseAddress = _baseAddress;      using (HttpResponseMessage response = client.GetAsync("api/v1/values").Result)      {          response.EnsureSuccessStatusCode();          string content = response.Content.ReadAsStringAsync().Result;          Console.WriteLine("Version 1 response: &apos;{0}&apos;\n", content);      }      using (HttpResponseMessage response = client.GetAsync("api/v2/values").Result)      {          response.EnsureSuccessStatusCode();          string content = response.Content.ReadAsStringAsync().Result;          Console.WriteLine("Version 2 response: &apos;{0}&apos;\n", content);      }}

参考资料:

http://blogs.msdn.com/b/webdev/archive/2013/03/08/using-namespaces-to-version-web-apis.aspx

https://mathieu.fenniak.net/aint-nobody-got-time-for-that-api-versioning/

http://aspnet.codeplex.com/SourceControl/changeset/view/dd207952fa86#Samples/WebApi/NamespaceControllerSelector/

http://bitoftech.net/2013/12/16/asp-net-web-api-versioning-strategy/

兴趣点

Web API的版本控制对我来说是我曾想到应提供给公众的服务中一个所必需的。实现 Web API 版本控制很有趣。我真的很爱即插即用或HTTP控制器选择器的更换。

0 0
原创粉丝点击