zoukankan      html  css  js  c++  java
  • Web API 接口版本控制 SDammann.WebApi.Versioning

    前言

     在设计对外 Web API 时,实务上可能会有新旧版本 API 并存的情况,例如开放 Web API 给厂商串接,但同一个服务更新版本时,不一定所有厂商可以在同一时间都跟着更新他们的系统,但如果直接把服务修改成新的,这些厂商可能就无法跟你的服务串 接了,直到他们修成新版的程序代码,他们方能正常运作。

    当这样的情况不被允许时,通常就会希望可以透过不同的 version 来呼叫「同一个 API 」,这里的同一个 API 包含了新旧版本的服务。

    目前的环境是 .NET framework 4.0, Web API 1.0, ASP.NET MVC 4,希望可以透过比较简单的方式来提供呼叫端可以简单易懂地明白:这是不同版本的同一个服务。

    开发上则希望能让 developer 很直觉地知道,怎么开发不同版本的服务,不需要管 routing 这件事,只要直接设计 controller 的内容即可。

    说明 

     在 Github 上找到一个能满足我这些基本需求的 open source: SDammann.WebApi.Versioning,相关的参考文章可以参考:

    ASP.NET Web API: Using Namespaces to Version Web APIs Versioning ASP.NET Web API Services Using HTTP Headers

    这个 solution 提供了两种定义 version 的方式:

    透过 routing, 也就是 url 上定义版本,以便分辨并找到对应的 controller 透过 request header, 好处是 url 不变,只要在 request 的 header 中指定 version, 就能找到对应的 controller

    这篇文章则带着大家快速、简单的 adopt 这个 solution。

    实作

    首先建立一个 ASP.NET MVC 4.0 的 project, 选择 Web API。

    接着透过 Nuget 安装 SDammann.WebApi.Versioning,NuGet 基本上只帮我们加入了相关参考的 dll 。

    接下来的动作超级简单,在 Controller 的文件夹中, 加入 Version1 的 folder ,因为 ControllerSelector 会依据 namespace 来找到对应的 Controller。建立好 Version1 的 folder 后,加入一个 HelloController ,如图所示:

    简单定义一个 Get 的方法,以及自定义回传的讯息格式 Message entity,程序代码如下所示:

    namespace WebApiWithVersioning.Controllers.Version1
    {
        public class HelloController : ApiController
        {
            public Message Get()
            {
                return new Message { Token = "Joey-v1", Signature = "91" };
            }
        }
        public class Message
        {
            public string Token { get; set; }
            public string Signature { get; set; }
        }
    }
    

    接着在 Controller 文件夹下新增一个 Version2 的 folder ,一样加入一个 HelloController,如图所示:

    一样定义一个 Get 的方法,但方法签章可以跟 Version1 的不一样,例如回传格式为  AnotherMessage, 程序代码如下所示:

    namespace VersioningWebApiSample.Controllers.Version2
    {
        public class HelloController : ApiController
        {
            public AnotherMessage Get()
            {
                return new AnotherMessage { NewToken = "Joey-v2", NewSignature = "91" };
            }
        }
        public class AnotherMessage
        {
            public string NewToken { get; set; }
            public string NewSignature { get; set; }
        }
    }
    

     透过 url routing 来决定呼叫的版本

    两个版本的 HelloController.Get() 都已经实作完毕了,接着只要让呼叫端可以决定要呼叫哪一个版本的Hello.Get() 即可。

    这里其实只要将 IApiExplorer 换成 VersionedApiExplorer ,接着视需求决定将 IHttpControllerSelector 换成 RouteVersionedControllerSelector 或是 AcceptHeaderVersionedControllerSelector ,前者是透过 url routing 来决定呼叫哪一个版本的服务,后者则是透过 request 的 accept header 来决定版本。

    上一篇文章 [ASP.NET Web API]3 分钟搞定 DI framework–Unity Application Block 已经介绍过,怎么快速地透过 Unity 来进行 DI 的动作,这边一样透过 NuGet 来安装 Unity.WebAPI ,接着在 Bootstrapper 中注册上面两个 interface 即可,程序代码如下所示:

     private static IUnityContainer BuildUnityContainer()
            {
                var container = new UnityContainer();
                // register all your components with the container here
                // e.g. container.RegisterType<ITestService, TestService>();            
                container.RegisterType<IApiExplorer, VersionedApiExplorer>(new InjectionConstructor(GlobalConfiguration.Configuration));
                container.RegisterType<IHttpControllerSelector, RouteVersionedControllerSelector>(new InjectionConstructor(GlobalConfiguration.Configuration));
                //container.RegisterType<IHttpControllerSelector, AcceptHeaderVersionedControllerSelector>(new InjectionConstructor(GlobalConfiguration.Configuration));
                
                return container;
            }
    

    上述的程序代码是先透过 routing 来区分版本。

    一样别忘了在 Global.asax 中,呼叫 Bootstrapper.Initialise() ,如下所示:

    public class WebApiApplication : System.Web.HttpApplication
        {
            protected void Application_Start()
            {
                Bootstrapper.Initialise();
                AreaRegistration.RegisterAllAreas();
                WebApiConfig.Register(GlobalConfiguration.Configuration);
                FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
                RouteConfig.RegisterRoutes(RouteTable.Routes);
                BundleConfig.RegisterBundles(BundleTable.Bundles);
            }
        }
    

    最后一个动作,则是修改 routing 方式,在 WebApiConfig 中,加入 version 的 routing rule ,程序代码如下所示:

      public static class WebApiConfig
        {
            public static void Register(HttpConfiguration config)
            {
                config.Routes.MapHttpRoute(
                                     name: "DefaultApiWithVersion",
                                     routeTemplate: "api/v{version}/{controller}/{action}/{id}",
                                     defaults: new { id = RouteParameter.Optional }
                         );
                config.Routes.MapHttpRoute(
                                    name: "DefaultApi",
                                    routeTemplate: "api/{controller}/{action}/{id}",
                                    defaults: new { id = RouteParameter.Optional }
                        );
                // Uncomment the following line of code to enable query support for actions with an IQueryable or IQueryable<T> return type.
                // To avoid processing unexpected or malicious queries, use the validation settings on QueryableAttribute to validate incoming queries.
                // For more information, visit http://go.microsoft.com/fwlink/?LinkId=279712.
                //config.EnableQuerySupport();
                // To disable tracing in your application, please comment out or remove the following line of code
                // For more information, refer to: http://www.asp.net/web-api
                config.EnableSystemDiagnosticsTracing();
            }
        }
    

    routeTemplate 的部分中间加入一个 /v{version}/ ,来分辨要呼叫哪一个版本即可。

    大功告成,来看一下怎么呼叫 Version1 的服务,如下图所示:

     url 为 /api/v1/Hello/Get ,回传的是内容是 Message 。

    那 Version2 的服务呢?如下图所示:

    url 为 /api/v2/Hello/Get ,回传的是内容是 AnotherMessage 。

    总结一句话:透过 url routing 来决定要呼叫哪一个 namespace 的 Controller ,就这么简单!

     透过 Request Header 来决定呼叫的版本

    如果希望 url 是固定的,只要变更 request header 就能呼叫不同版本的服务,在这个 solution 中也相当简单。

    首先,将 container 注册 IHttpControllerSelector 的实体,换成 AcceptHeaderVersionedControllerSelector ,如下所示:

    public static class Bootstrapper
        {
            public static void Initialise()
            {
                var container = BuildUnityContainer();
                GlobalConfiguration.Configuration.DependencyResolver = new Unity.WebApi.UnityDependencyResolver(container);
            }
            private static IUnityContainer BuildUnityContainer()
            {
                var container = new UnityContainer();
                // register all your components with the container here
                // e.g. container.RegisterType<ITestService, TestService>();            
                container.RegisterType<IApiExplorer, VersionedApiExplorer>(new InjectionConstructor(GlobalConfiguration.Configuration));
                //container.RegisterType<IHttpControllerSelector, RouteVersionedControllerSelector>(new InjectionConstructor(GlobalConfiguration.Configuration));
                container.RegisterType<IHttpControllerSelector, AcceptHeaderVersionedControllerSelector>(new InjectionConstructor(GlobalConfiguration.Configuration));
                return container;
            }
        }
    

    没了,就这样,接着透过 Help Page 的 Test Client 来测试看看。(Test Client 只需要透过 NuGet 安装 A Simple Test Client for ASP.NET Web API 即可,如下图所示)

    接着打开 api/v1/Hello/Get 的说明页面,如图所示:

    将 url 修改成:api/Hello/Get ,并加入 accept header: application/json; version=1 ,如下图所示:

    点下 Send 后,就可以看到呼叫 Version1 的结果,如下图所示:

    我们将刚刚的 header 改成 version=2 ,则出来的结果就会是 Version2 的结果,如下图所示:

    呼叫不同版本的服务,就这么简单,要切换版本对应的方式,只要修改 container 注册的部分即可,这也是透过 DI framework 的强大方便之处。

     延伸议题

    这个 solution 有一个缺点,就是 Help Page 会多出很多奇怪的 API ,如下图所示:

    这几个事实上都不应该被呼叫,也无法正常被呼叫。

    这里简单的修改一下 Help Page 的内容,让读者可以看到我们期望的 Help Page 样子,设计方式要如何弹性,可以请读者自行思考。

    首先打开 Areas.HelpPage 的 ApiDescriptionExtensions ,加入一个扩充方法 CheckIsVersionControllerValid 来判断:

    Controller 的 namespace 是不是有含 Version routeTemplate 是不是有含 Version

    程序代码如下所示:

         //todo by joey, 新增为了version controller的处理, 目前是hard-code硬干,未来可修改成regex
            public static bool CheckIsVersionControllerValid(this ApiDescription description)
            {
                var controllerName = (description.ActionDescriptor).ControllerDescriptor.ControllerType.FullName;
                var routeTemplate = (description.Route).RouteTemplate;
                var isControllerWithVersion = controllerName.Contains(".Version");
                var isRouteTemplateWithVersion = routeTemplate.Contains("{version}");
                return isControllerWithVersion == isRouteTemplateWithVersion;
            }
    

    接着修改 ApiGroup.cshtml ,加入判断如果 Controller 的 type 与 Version 的 routeTemplate 是吻合的,才显示在 Help Page 上,程序代码如下图所示:

    出来的 Help Page 就是我们要的,如下图所示:

    下一步我们希望根据版本号显示对应的帮助文档

    修改 HelpController.cs 中的 Index

            public ActionResult Index(string version)
            {
                var model = Configuration.Services.GetApiExplorer().ApiDescriptions;
                if (!string.IsNullOrEmpty(version))
                {
                    var oldapis = Configuration.Services.GetApiExplorer().ApiDescriptions;
                    foreach (var oldapi in oldapis)
                    {
                        if (!oldapi.RelativePath.Contains("/" + version + "/"))
                        {
                            model.Remove(oldapi);
                        }
                    }
                }
                ViewBag.DocumentationProvider = Configuration.Services.GetDocumentationProvider();
                return View(model);
            }

    接着修改路由 HelpPageAreaRegistration.cs

            public override void RegisterArea(AreaRegistrationContext context)
            {
                context.MapRoute(
                    "HelpPage_Version",
                    "Help/{version}",
                    new { controller = "Help", action = "Index", version = UrlParameter.Optional });
    
                context.MapRoute(
                    "HelpPage_Default",
                    "Help/{action}/{apiId}",
                    new { controller = "Help", action = "Index", apiId = UrlParameter.Optional });
    
                
    
                HelpPageConfig.Register(GlobalConfiguration.Configuration);
            }

    结果

    ---------------------------------------

    结论

    希望这个简单、方便的方式,可以满足多版本同时服务的需求,也可以让呼叫端能够简便、好懂地呼叫不同版本的服务,而 server 端的切换与设计也可以相当单纯, developer 几乎只要新增不同 Version 的文件夹,继续往下写 controller 的内容即可。

  • 相关阅读:
    js 获取图片url的Blob值并预览
    Xcode工程编译错误:“Cannot assign to 'self' outside of a method in the init family”
    iOS-原生纯代码约束总结(二)之 AutoLayout
    iOS-原生纯代码约束总结(一)之 AutoresizingMask
    iOS 动画学习之视图控制器转场动画
    ios开发之 NSObject详解
    mac终端下svn常用命令
    CALayer的子类之CAShapeLayer
    Runloop的再学习之浅析(一)
    Xcode 编辑器之Workspace,Project,Scheme,Target
  • 原文地址:https://www.cnblogs.com/ideacore/p/6434294.html
Copyright © 2011-2022 走看看