前言

在设计对外 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 的内容即可。

Web API 接口版本控制 SDammann.WebApi.Versioning的更多相关文章

  1. Http下的各种操作类.WebApi系列~通过HttpClient来调用Web Api接口

    1.WebApi系列~通过HttpClient来调用Web Api接口 http://www.cnblogs.com/lori/p/4045413.html HttpClient使用详解(java版本 ...

  2. WebApi系列~通过HttpClient来调用Web Api接口~续~实体参数的传递

    回到目录 上一讲中介绍了使用HttpClient如何去调用一个标准的Web Api接口,并且我们知道了Post,Put方法只能有一个FromBody参数,再有多个参数时,上讲提到,需要将它封装成一个对 ...

  3. ASP.NET Web API 接口执行时间监控

    软件产品常常会出现这样的情况:产品性能因某些无法预料的瓶颈而受到干扰,导致程序的处理效率降低,性能得不到充分的发挥.如何快速有效地找到软件产品的性能瓶颈,则是我们感兴趣的内容之一. 在本文中,我将解释 ...

  4. 不使用jQuery对Web API接口POST,PUT,DELETE数据

    前些天,Insus.NET有演示Web API接口的操作: <怎样操作WebAPI接口(显示数据)>http://www.cnblogs.com/insus/p/5670401.html ...

  5. Winform混合式开发框架访问Web API接口的处理

    在我的混合式开发框架里面,集成了WebAPI的访问,这种访问方式不仅可以实现简便的数据交换,而且可以在多种平台上进行接入,如Winform程序.Web网站.移动端APP等多种接入方式,Web API的 ...

  6. Web API接口设计经验总结

    在Web API接口的开发过程中,我们可能会碰到各种各样的问题,我在前面两篇随笔<Web API应用架构在Winform混合框架中的应用(1)>.<Web API应用架构在Winfo ...

  7. 如何让你的 Asp.Net Web Api 接口,拥抱支持跨域访问。

    由于 web api 项目通常是被做成了一个独立站点,来提供数据,在做web api 项目的时候,不免前端会遇到跨域访问接口的问题. 刚开始没做任何处理,用jsonp的方式调用 web api 接口, ...

  8. Asp.Net Web Api 接口,拥抱支持跨域访问。

    如何让你的 Asp.Net Web Api 接口,拥抱支持跨域访问. 由于 web api 项目通常是被做成了一个独立站点,来提供数据,在做web api 项目的时候,不免前端会遇到跨域访问接口的问题 ...

  9. Asp.Net Web Api 接口

    如何让你的 Asp.Net Web Api 接口,拥抱支持跨域访问.   由于 web api 项目通常是被做成了一个独立站点,来提供数据,在做web api 项目的时候,不免前端会遇到跨域访问接口的 ...

随机推荐

  1. Flutter点击两次返回键退出APP

    在APP中一些页面为了防止用户操作失误点击到返回键导致退出APP,可以设置其一定时间内点击两次返回键才允许退出APP,完成这个功能可以通过WillPopScope和SystemNavigator.po ...

  2. Python 自学笔记(二)

    3-1.条件判断 3-1.条件判断 3-1-1.单项判断 if 3-1-2.双向判断 if...else... 3-1-3.多向判断 if...elif...else 3-2.if嵌套 4.输入 4- ...

  3. mongoose 实现 增、删、改、查

    mongoose常用的API 增 save是一个实例方法,使用时需要先 new Model() 来实例化 //保存一个用户信息,userobj为你创建的文档对象模型里的字段,需正确对应传入 const ...

  4. python 调用父类方法, 重写父类构造方法, 不显式调用,会报错

    子类不显式调用父类的构造方法,而父类构造函数初始化了一些属性,就会出现问题 如果子类和父类都有构造函数,子类其实是重写了父类的构造函数,如果不显式调用父类构造函数,父类的构造函数就不会被执行,导致子类 ...

  5. cookie和session的区别及其原理

    1.为什么要有cookie/session? HTTP是一种无状态的协议,为了分辨链接是谁发起的,需自己去解决这个问题.不然有些情况下即使是同一个网站每打开一个页面也都要登录一下.而Session和C ...

  6. labelme

    项目:https://github.com/wkentaro/labelme?tdsourcetag=s_pcqq_aiomsg 说明:https://www.bilibili.com/video/a ...

  7. 实现在线阅读WORD,PDF等文件,JAVA,PHP都可以

    1 <?php 2 //header("Content-type:text/html;charset=utf-8"); 3 //word转html 展示 4 $lj=$_GE ...

  8. MySQL问题:Access denied for user 'mysql'@'localhost'

    deep@deep-PC:~$ sudo mysql -uroot -p mysql> update mysql.user set authentication_string=PASSWORD( ...

  9. 前端面试经典题之ES6新特性

    ES6 主要是为了解决 ES5 的先天不足,在原先ES5的基础上新增了许多内容,本篇文章将列举出ES6中新增的10大特性. 一. let 和 const 与var不同,let和const都是用于命名局 ...

  10. 【并行计算与CUDA开发】英伟达硬件加速解码器在 FFMPEG 中的使用

    目录(?)[-] 私有驱动 编译 FFMPEG 使用 nvenc 这篇文档介绍如何在 ffmpeg 中使用 nvenc 硬件编码器. 私有驱动 nvenc 本身是依赖于 nvidia 底层的私有驱动的 ...