ASP.NET Core 實戰:構建帶有版本控制的 API 介面
一、前言
在上一篇的文章中,主要是搭建了我們的開發環境,同時建立了我們的專案模板框架。在整個前後端分離的專案中,後端的 API 介面至關重要,它是前端與後端之間進行溝通的媒介,如何構建一個 “好用” 的 API 介面,是需要我們後端人員好好思考的。
在系統迭代的整個過程中,不可避免的會新增新的資源,或是修改現有的資源,後端介面作為暴露給外界的服務,變動的越小,對服務的使用方造成的印象就越小,因此,如何對我們的 API 介面進行合適的版本控制,我們勢必需要首先考慮。
系列目錄地址:ASP.NET Core 專案實戰
倉儲地址:https://github.com/Lanesra712/Grapefruit.VuCore
二、Step by Step
專案總是在不斷迭代的,某些時候,因為業務發展的需要,需要將現有的介面進行升級,而原有的介面卻不能立刻停止使用。比如說,你開發了一個介面提供給愛啪啪 1.0 版本使用,後來愛啪啪的版本迭代了,需要介面返回的資料與原先 1.0 版本返回的資料不同了,這時候,介面肯定是需要升級的,可是如果直接升級原有的介面,還在使用 1.0 版本的使用者不就 GG 了,因此,如何做到既可以讓 1.0 版本的使用者使用,也可以讓 2.0 版本的使用者使用就需要好好考慮了,常見的解決方案,主要有以下幾種。
a)使用不同的 API 名稱
最簡單粗暴,需要變更介面邏輯時就重新起個 API 名稱,新的版本呼叫新的 API 名稱,舊的版本呼叫舊的 API 名稱。
https://yuiter.com/api/Secret/Login ##愛啪啪 1.0
https://yuiter.com/api/Secret/NewLogin ##愛啪啪 2.0
b)在 Url 中標明版本號
直接將 API 版本資訊新增到請求的 Url 中,呼叫不同版本的 API ,就在 URL 中直接標明使用的是哪個版本。
https://yuiter.com/api/v1/Secret/Login ##愛啪啪 1.0
https://yuiter.com/api/v2/Secret/Login ##愛啪啪 2.0
c)請求引數中新增版本資訊
將 API 的版本資訊作為請求的一個引數傳遞,通過指定引數值來確定請求的 API 版本。
https://yuiter.com/api/Secret/Login?version=1 ##愛啪啪 1.0
https://yuiter.com/api/Secret/Login?version=2 ##愛啪啪 2.0
d)在 header 中標明版本號
前端在請求 API 介面時,在 header 中新增一個引數用來表明請求的版本資訊,後端通過前端在 header 中設定的引數來判斷,從而執行不同的業務邏輯分支。
POST https://yuiter.com/api/Secret/Login
Host: yuiter.com
api-version: v1 ##愛啪啪 1.0
POST https://yuiter.com/api/Secret/Login
Host: yuiter.com
api-version: v2 ##愛啪啪 2.0
在 Grapefruit.VuCore 這個專案中,我選擇將 API 的版本資訊新增到請求的地址中,從而明確的指出當前請求的介面版本資訊。
1、Swagger 整合
後端完成了介面之後,肯定需要告訴前端,不管是整理成 txt/excel/markdown 文件,亦或是寫完一個介面就直接發微信告訴前端,總是要多做一步的事情,而 Swagger 則可以幫我們省去這一步。通過配置之後,Swagger 就可以根據我們的介面自動生成 API 的介面文件,省時,省力。當然,如果前端小姐姐單身可撩,而你碰巧有意的話,另談。
Swagger 是一個可以將介面文件自動生成,同時可以對介面功能進行測試的開源框架,在 ASP.NET Core 環境下,主流的有 Swashbuckle.AspNetCore 和 NSwag 這兩個開源框架幫助我們生成 Swagger documents。這裡,我採用的是 Swashbuckle.AspNetCore。
在使用 Swashbuckle.AspNetCore 之前,首先我們需要在 API(Grapefruit.WebApi) 專案中新增對於 Swashbuckle.AspNetCore 的引用。你可以直接右鍵選中 API 專案選擇管理 Nuget 程式包進行載入引用,也可以通過程式包管理控制檯進行新增引用,這裡注意,使用程式包管理控制檯時,你需要將預設的專案修改成 API(Grapefruit.WebApi) 專案。當引用新增完成後,我們就可以在專案中配置 Swagger 了。
Install-Package Swashbuckle.AspNetCore
ASP.NET Core 的本質上可以看成是一個控制檯程式,在我們建立好的 ASP.NET Core Web API 專案中,存在著兩個類檔案:Program.cs 以及 Startup.cs。與控制檯應用一樣,Program 類中的 Main 方法是整個程式的入口,在這個方法中,我們將配置好的 IWebHostBuilder 物件,構建成 IWebHost 物件,並執行該 IWebHost 物件從而達到執行 Web 專案的作用。
在框架生成的 Program 類檔案中,在配置 IWebHostBuilder 的過程時,框架預設為我們添加了一些服務,當然,這裡你可以註釋掉預設的寫法,去自己建立一個 WebHostBuilder 物件。同時,對於一個 ASP.NET Core 程式來說,Startup 類是必須的(你可以刪除生成的 Startup 類,重新建立一個新的類,但是,這個新建立的類必須包含 Configure 方法,之後只需要在 UseStartup<Startup> 中將該類配置為 Startup 類即可),這裡如果不指定 Startup 類會導致啟動失敗。
在 Startup 類中,存在著 ConfigureServices 和 Configure 這兩個方法,在 ConfigureServices 方法中,我們將自定義服務通過依賴注入的方式新增到 IServiceCollection 容器中,而這些容器中的服務,最終都可以在 Configure 方法中進行使用;而 Configure 方法則用於指定 ASP.NET Core 應用程式將如何響應每一個 HTTP 請求,我們可以在這裡將我們自己建立的中介軟體(Middleware)繫結到 IApplicationBuilder 上,從而新增到 HTTP 請求管道中。
這裡只是很粗略的說明了 ASP.NET Core 專案的啟動過程,想要仔細瞭解啟動過程的推薦園子裡的這篇文章 =》ASP.NET Core 2.0 : 七.一張圖看透啟動背後的祕密,因為 ASP.NET Core 2.1 版本相比於 2.0 版本又有些改變,這裡有一些不一樣的地方需要你去注意。
當我們簡單瞭解了啟動過程後,就可以配置我們的 Swagger 了。Swashbuckle.AspNetCore 幫我們構建好了使用 Swagger 的中介軟體,我們只需要直接使用即可。
首先我們需要在 ConfigureServices 方法中,將我們的服務新增到 IServiceCollection 容器中,這裡,我們需要為生成的 Swagger Document 進行一些配置。
services.AddSwaggerGen(s => { s.SwaggerDoc("v1", new Info { Contact = new Contact { Name = "Danvic Wang", Email = "[email protected]", Url = "https://yuiter.com" }, Description = "A front-background project build by ASP.NET Core 2.1 and Vue", Title = "Grapefruit.VuCore", Version = "v1" }); });
之後,我們就可以在 Configure 方法中啟用我們的 Swagger 中介軟體。
app.UseSwagger(); app.UseSwaggerUI(s => { s.SwaggerEndpoint("/swagger/v1/swagger.json", "Grapefruit.VuCore API V1.0"); });
此時,當你執行程式,在域名後面輸入/swagger 即可訪問到我們的 API 文件頁面。因為專案啟動時預設訪問的是我們 api/values 的 GET 請求介面,這裡我們可以開啟 Properties 下的 launchSetting.json 檔案去配置我們的程式預設開啟頁面。
從上面的圖可以看出,不管是使用 IIS 或是程式自託管,我們預設開啟的 Url 都是 api/values,這裡我們將兩種啟動方式的 launchUrl 值都修改成 swagger 之後再次執行我們的專案,可以發現,程式預設的開啟頁面就會變成我們的 API 文件頁面。
我們使用 API 文件的目的,就是為了讓前端知道請求的方法地址是什麼,需要傳遞什麼引數,而現在,並沒有辦法顯示出我們對於引數以及方法的註釋,通過檢視 Swashbuckle.AspNetCore 的 github 首頁可以看到,我們可以通過配置,將生成的 json 檔案中包含我們對於 Controller or Action 的 Xml 註釋內容,從而達到顯示註釋資訊的功能(最終呈現的 Swagger Doc 是根據之前我們定義的這個 “/swagger/v1/swagger.json” json 檔案來生成的)。
右鍵我們的 API 專案,屬性 =》生產,勾選上 XML 文件檔案,系統會預設幫我們建立生成 XML 檔案的地址,這時候,我們重新生成專案,則會發現,當前專案下會多出這個 XML 檔案。在重新生成專案的過程中,你會發現,錯誤列表會顯示很多警告資訊,提示我們一些方法沒有新增 XML 註釋。如果你和我一樣強迫症的話,可以把 1591 這個錯誤新增到上面的禁止顯示警告中,這樣就可以不再顯示這個警告了。
建立好 XML 的註釋檔案後,我們就可以配置我們的 Swagger 文件,從而達到顯示註釋的功能。這裡,因為我會在 Grapefruit.Application 類庫中建立各種的 Dto 物件,而介面中是會呼叫到這些 Dto 物件的。因此,為了顯示這些 Dto 上的註釋資訊,這裡我們也需要生成 Grapefruit.Application 專案的 XML 註釋檔案。
PS:這裡我是將每個專案生成的註釋資訊 xml 文件地址都放在了程式的基礎路徑下,如果你將 xml 文件生成在別的位置,這裡獲取 xml 的方法就需要你進行修改。
services.AddSwaggerGen(s => { //... //Add comments description // var basePath = Path.GetDirectoryName(AppContext.BaseDirectory);//get application located directory var apiPath = Path.Combine(basePath, "Grapefruit.WebApi.xml"); var dtoPath = Path.Combine(basePath, "Grapefruit.Application.xml"); s.IncludeXmlComments(apiPath, true); s.IncludeXmlComments(dtoPath, true); });
當我們把 Swagger 配置完成之後,我們就可以建立具有版本控制的 API 介面了。
2、帶有版本控制的 API 介面實現
在請求的 API Url 中標明版本號,我不知道你第一時間看到這個實現方式,會想到什麼,對於我來說,直接在路由資訊中新增版本號不就可以了。。。em,這樣過於投機取巧了。。。。
[Route("api/v1/[controller]")]//新增版本資訊為v1 [ApiController] public class ValuesController : ControllerBase { }
想了想,在 Url 中新增版本號,這個版本號是不是很像我們在 MVC 中使用的 Area。
Area 是 MVC 中經常使用到的一個功能,我們通常會將某些小的模組拆分成一個個的 Area,而這一個個的小 Area 其實就是這個 MVC 專案中的 MVC。通過為 controller 和 action 新增另一個路由引數 area,從而達到建立具有層次路由的結構。比如,這裡,我們可以建立一個 Area 叫 v1,用來儲存我們 1.x 版本的 API 介面,之後如果有新的 API 版本,新增一個 Area 即可,是不是很簡單,嗯,說幹就幹。
右擊我們的 API 專案,選擇新增區域,新增的 Area 名稱為 v1。
當 ASP.NET Core 的腳手架程式新增完成 Area 後,則會開啟一個檔案提示我們需要在 MVC 中介軟體中建立適用於 Area 的路由定義。
app.UseMvc(routes => { routes.MapRoute( name : "areas", template : "{area:exists}/{controller=Home}/{action=Index}/{id?}" ); });
當我們新增好路由規則定義後,我們在 Area 的 Controllers 資料夾下新增一個 WebAPI Controller。不同於 ASP.NET 中的 Area ,當我們在 ASP.NET Core 建立好一個 Area 之後,腳手架生成的檔案中不再有 XXXAreaRegistration(XXX 為 Area 的名稱)檔案去註冊這個 Area,而我們只需要在 Area 中的 Controller 中新增 Area 特性,即可告訴系統框架,這個 Controller 是在當前的 Area 下的。
如果你有自己嘗試的話,就會發現,當我們建立好一個 v1 的 Area 後,這個請求的地址並沒有按照我們的想法會體現在路由資訊中,我們最後還是需要在 Route 中手動指明 API 版本。
[Area("v1")] [Route("api/v1/[controller]")] [ApiController] public class ValuesController : ControllerBase { }
這樣的話,和最開始直接在路由資訊中寫死版本資訊其實也就沒什麼差別了,上網搜了搜,發現巨硬爸爸,也早已為我們準備好了實現版本控制 API 的利器 - Microsoft.AspNetCore.Mvc.Versioning。
和上面使用 Swashbuckle.AspNetCore 的方式相同,在我們使用 Versioning 之前,需要在我們的 API 專案中新增對於該 dll 的引用。這裡需要注意下安裝的版本問題,因為 Grapefruit.VuCore 這個框架距離現在搭建也有幾個月的時間了,在這個月初的時候 .NET Core 2.2 也已經發布了,如果你和我一樣還是採用的 .NET Core 2.1 版本的話,這裡安裝的 Versioning 版本最高只能到 2.3。
Install-Package Microsoft.AspNetCore.Mvc.Versioning
當我們安裝完成之後,就可以進行配置了。
public void ConfigureServices(IServiceCollection services) { services.AddApiVersioning(o => { o.ReportApiVersions = true;//return versions in a response header o.DefaultApiVersion = new ApiVersion(1, 0);//default version select o.AssumeDefaultVersionWhenUnspecified = true;//if not specifying an api version,show the default version }); }
ReportApiVersions:這個配置是可選的,當我們設定為 true 時,API 會在響應的 header 中返回版本資訊。
DefaultApiVersion:指定在請求中未指明版本時要使用的預設 API 版本。這將預設版本為1.0。
AssumeDefaultVersionWhenUnspecified:這個配置項將用於在沒有指明 API 版本的情況下提供請求,預設情況下,會請求預設版本的 API,例如,這裡就會請求 1.0 版本的 API。
這裡,刪除我們之前的建立的 Area 和預設的 ValuesController,在 Controllers 資料夾下新增一個 v1 資料夾,將所有 v1 版本的 Controller 都建在這個目錄下。新建一個 Controller,新增上 ApiVersion Attribute 指明當前的版本資訊。因為我採用的方案是在 Url 中指明 API 版本,所以,我們還需要在 Route 中修改我們的路由屬性以對應 API 的版本。這裡的 v 只是一個預設的慣例,你也可以不新增。
[ApiVersion("1.0")] [Route("api/v{version:apiVersion}/[controller]")] [ApiController] public class VaulesController : ControllerBase { }
當我們修改好我們的 Controller 之後,執行我們的專案,你會發現,API 文件中顯示的請求地址是不對的,難道是我們的配置沒起作用嗎?通過 Swagger 自帶的 API 測試工具測試下我們的介面,原來這裡請求的 Url 中已經包含了我們定義的版本資訊,當我們指定錯誤的版本資訊時,工具也會告訴我們這個版本的介面不存在。
雖然我們請求的 Url 中已經帶上了版本資訊,但是 API 文件上顯示的請求地址卻是不準確的,強迫症,不能忍。這裡,需要我們修改生成 Swagger 文件的配置程式碼,將路由中的版本資訊進行替換。重新執行我們的專案,可以發現,文件顯示的 Url 地址也已經正確了,自此,我們建立帶有版本控制的 API 也就完成了。
public void ConfigureServices(IServiceCollection services) { services.AddSwaggerGen(s => { //... //Show the api version in url address s.DocInclusionPredicate((version, apiDescription) => { var values = apiDescription.RelativePath .Split('/') .Select(v => v.Replace("v{version}", version)); apiDescription.RelativePath = string.Join("/", values); return true; }); }); }
三、總結
本章使用了 Microsoft.AspNetCore.Mvc.Versioning 這一元件來實現我們對於 API 版本控制的功能實現,可能你會有疑問,我們直接在路由中寫明版本資訊不是更簡單嗎?在我看來,使用這一元件的目的,在於我們可以以多種的方式實現 API 版本控制的目的,如果哪天你不想在 Url 中指明版本資訊後,你可以很快的使用別的形式來完成 API 的版本控制。另外,直接在路由中寫上版本資訊,是不是會顯得我們比較 ‘low’,哈哈哈,開玩笑,最後祝大家聖誕快樂~~~