[ASP.NET Web API]Versioning Web API by SDammann.WebApi.Versioning

  • 7277
  • 0
  • 2013-10-28

[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,相關的參考文章可以參考:

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

這個 solution 提供了兩種定義 version 的方式:

  1. 透過 routing, 也就是 url 上定義版本,以便分辨並找到對應的 controller
  2. 透過 request header, 好處是 url 不變,只要在 request 的 header 中指定 version, 就能找到對應的 controller

這篇文章則帶著大家快速、簡單的 adopt 這個 solution。

 

實作

首先建立一個 ASP.NET MVC 4.0 的 project, 選擇 Web API。

接著透過 Nuget 安裝 SDammann.WebApi.Versioning,如圖所示:

image

NuGet 基本上只幫我們加入了相關參考的 dll 。

接下來的動作超級簡單,在 Controller 的資料夾中,加入 Version1 的 folder ,因為 ControllerSelector 會依據 namespace 來找到對應的 Controller。建立好 Version1 的 folder 後,加入一個 HelloController ,如圖所示:

image

image

簡單定義一個 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,如圖所示:

image

image

一樣定義一個 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 的服務,如下圖所示:

image

url 為 /api/v1/Hello/Get ,回傳的是內容是 Message 。

那 Version2 的服務呢?如下圖所示:

image

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 即可,如下圖所示)
image

接著打開 api/v1/Hello/Get 的說明頁面,如圖所示:

image

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

image

點下 Send 後,就可以看到呼叫 Version1 的結果,如下圖所示:

image

我們將剛剛的 header 改成 version=2 ,則出來的結果就會是 Version2 的結果,如下圖所示:

image

呼叫不同版本的服務,就這麼簡單,要切換版本對應的方式,只要修改 container 註冊的部分即可,這也是透過 DI framework 的強大方便之處。

 

延伸議題

這個 solution 有一個缺點,就是 Help Page 會多出很多奇怪的 API ,如下圖所示:

image

這幾個事實上都不應該被呼叫,也無法正常被呼叫。

這裡簡單的修改一下 Help Page 的內容,讓讀者可以看到我們期望的 Help Page 樣子,設計方式要如何彈性,可以請讀者自行思考。

首先打開 Areas.HelpPage 的 ApiDescriptionExtensions ,加入一個擴充方法 CheckIsVersionControllerValid 來判斷:

  1. Controller 的 namespace 是不是有含 Version
  2. 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 上,程式碼如下圖所示:

image

出來的 Help Page 就是我們要的,如下圖所示:

image

 

結論

希望這個簡單、方便的方式,可以滿足多版本同時服務的需求,也可以讓呼叫端能夠簡便、好懂地呼叫不同版本的服務,而 server 端的切換與設計也可以相當單純, developer 幾乎只要新增不同 Version 的資料夾,繼續往下寫 controller 的內容即可。

 

影片

 

Sample Code


blog 與課程更新內容,請前往新站位置:http://tdd.best/