[ASP.NET Web API]Versioning Web API by 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 就是我們要的,如下圖所示:
結論
希望這個簡單、方便的方式,可以滿足多版本同時服務的需求,也可以讓呼叫端能夠簡便、好懂地呼叫不同版本的服務,而 server 端的切換與設計也可以相當單純, developer 幾乎只要新增不同 Version 的資料夾,繼續往下寫 controller 的內容即可。
影片
blog 與課程更新內容,請前往新站位置:http://tdd.best/