一、前言
在目前的軟體開發的潮流中,不管是前後端分離還是服務化改造,後端更多的是通過構建 API 介面服務從而為 web、app、desktop 等各種客戶端提供業務支援,如何構建一個符合規範、容易理解的 API 介面是我們後端開發人員需要考慮的。在本篇文章中,我將列舉一些我在使用 ASP.NET Core Web API 構建介面服務時使用到的一些小技巧,因才疏學淺,可能會存在不對的地方,歡迎指出。
程式碼倉儲:https://github.com/Lanesra712/ingos-server
二、Step by Step
因為本篇文章中涉及到的一些知識點在之前的文章中也已經有具體的解釋了,所以這裡只會說明如何在 ASP.NET Core Web API 中如何去使用,不會做過多的詳細介紹。如果你需要詳細瞭解的話,可以跳轉到文章中給出的外鏈地址去檢視。
本篇文章中使用的程式碼是基於 .NET Core 2.2 + .NET Standard 2.0 進行構建的,如果你採用的版本與我使用的不同,可能最終實現起來的程式碼會有所不同,請提前知悉。同時,本篇文章中所有示例程式碼都會存在於前言中所列出的 github repo 中,我會嘗試將每個功能點的開發作為一次 commit,並且也會在後續進行不定期的更新完善,最終搭建一個基於領域驅動思想的後端專案模板,如果對你有幫助的話,歡迎持續關注。
1、使用小寫路由
在我之前的一篇文章中(構建可讀性更高的 ASP.NET Core 路由)有提到過,因為 .NET 預設採用 Pascal 的類命名方式,如果採用預設生成的路由,最終構建出的路由地址會存在大小寫混在一起的情況,雖然在 .NET Core 中大小寫的路由地址最終都會對於到正確的資源上,但是為了更好的符合前端的規範,所以這裡我們首先按照之前的文章中所列出的方法去修改預設生成的路由地址格式。
因為這裡我們最終想要實現的是符合 Restful 風格的 API 介面,所以這裡我們首先需要將預設生成的 URL 地址改為全小寫模式。
public void ConfigureServices(IServiceCollection services) { // 採用小寫的 URL 路由模式 services.AddRouting(options => { options.LowercaseUrls = true; }); }
如果你有看過構建可讀性更高的 ASP.NET Core 路由這篇文章,你會發現其實我們最終實現的是 hyphen(-) 格式的 Url 地址,那麼這裡我們為什麼不進行後續的修改了呢?
如果你有檢視 .NET Core 預設模板中生成的 API Controller,仔細看下,這裡其實是使用的特性路由,所以這裡我們並不能通過 Startup.UseMvc 定義的傳統路由模板,或是直接在 Startup.Configure 中的 UseMvcWithDefaultRoute 方法去修改我們的生成的路由地址格式。
[Route("api/[controller]")] [ApiController] public class ValuesController : ControllerBase { }
2、允許跨域請求
不管是後端介面的服務化改造,還是隻是單純的前後端分離專案開發,我們的前端專案與後端介面通常不會部署在一起,所以我們需要解決前端訪問介面時會涉及到的跨域訪問的問題。
針對跨域請求,我們可以採用 jsonp、或者是通過給 nginx 伺服器配置響應的 header 引數頭資訊、或者是使用 CORS,又或是其它的解決方案。你可以自由選擇,這裡我採用在後端介面中直接配置對於 CORS 的支援。
在 .NET Core 中,已經在 Microsoft.AspNetCore.Cors 這個類庫中新增了對於 CORS 的支援,因為這個類庫是存在於我們已經安裝的 .NET Core SDK 中,所以這裡我們並不需要通過 Nuget 進行安裝,可以直接使用。
在 .NET Core 中配置 CORS 規則,我們可以通過在 Startup.ConfigureServices 這個方法中新增不同的授權策略,之後再針對某個 Controller 或是 Action 通過新增 EnableCors 這個 Attribute 的方式進行配置,這裡如果指定了 policy 策略名稱,則會使用指定的策略,如果沒有指定,則適用於系統的預設配置。同樣的,我們也可以只設定一個策略,直接針對整個專案進行配置,這裡我採用對整個專案採用通用的跨域請求配置方案。
在配置 CORS 策略時,我們可以設定只允許來源於某些 URL 地址的請求可以訪問,或者是指定介面只允許某些 HTTP 方法進行訪問,或者是在請求的 header 中必須包含某些資訊才可以訪問我們的介面。
在下面的程式碼中,我定義了針對整個專案的跨域請求策略,這裡我只是設定了對於介面請求方 URL 地址的控制,通過讀取配置檔案中的資料,從而達到只允許某些 IP 可以訪問的我們介面的目的。
public class Startup { // 預設的跨域請求策略名稱 private const string _defaultCorsPolicyName = "Ingos.Api.Cors"; // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddMvc( // 新增 CORS 授權過濾器 options => options.Filters.Add(new CorsAuthorizationFilterFactory(_defaultCorsPolicyName)) ).SetCompatibilityVersion(CompatibilityVersion.Version_2_2); // 配置 CORS 授權策略 services.AddCors(options => options.AddPolicy(_defaultCorsPolicyName, builder => builder.WithOrigins( Configuration["Application:CorsOrigins"] .Split(",", StringSplitOptions.RemoveEmptyEntries).ToArray() ) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials())); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // 允許跨域請求訪問 app.UseCors(_defaultCorsPolicyName); } }
例如在下面的設定中,我只允許這一個地址可以訪問我們的介面,如果需要指定多個的話,則可以通過英文的 , 進行分隔。
"Application": { "CorsOrigins": "http://127.0.0.1:5050" }
某些情況下,如果我們不想進行限制的話,只需要將值改為 * 即可。
"Application": { "CorsOrigins": "*" }
3、新增介面版本控制
在一些涉及到介面功能升級的場景下,當我們需要修改介面邏輯而舊版本的介面無法停用的情況時,為了減少對於原有介面的影響,我們可以採取為介面新增版本資訊的形式,從而降低因採用不同版本而造成的影響。如果你想要詳細瞭解的話,可以檢視這篇文章,電梯直達 =》ASP.NET Core 實戰:構建帶有版本控制的 API 介面。
在實現具有版本控制的介面前,首先我們需要通過 Nuget 新增下面的兩個 dll,因為我是在 Ingos.Api.Core 這個類庫中進行配置的,所以我安裝到了這個類庫下,你需要根據你自己的情況選擇最終是安裝到 Api 介面專案中還是在別的類庫下。
Install-Package Microsoft.AspNetCore.Mvc.Versioning
Install-Package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
在安裝完成之後,我們就可以在 Startup.ConfigureServices 方法中,為專案中的介面配置版本資訊,這裡我採用的方案是將版本號新增到介面的 URL 地址中。
因為對於所有中介軟體的配置都會在 Startup.ConfigureServices 方法中,為了保持該方法的純淨性,這裡我寫了一個擴充套件方法用於配置我們的 api 的版本,之後直接呼叫即可。
public static class ApiVersionExtension { /// <summary> /// 新增 API 版本控制擴充套件方法 /// </summary> /// <param name="services">生命週期中注入的服務集合 <see cref="IServiceCollection"/></param> public static void AddApiVersion(this IServiceCollection services) { // 新增 API 版本支援 services.AddApiVersioning(o => { // 是否在響應的 header 資訊中返回 API 版本資訊 o.ReportApiVersions = true; // 預設的 API 版本 o.DefaultApiVersion = new ApiVersion(1, 0); // 未指定 API 版本時,設定 API 版本為預設的版本 o.AssumeDefaultVersionWhenUnspecified = true; }); // 配置 API 版本資訊 services.AddVersionedApiExplorer(option => { // api 版本分組名稱 option.GroupNameFormat = "'v'VVVV"; // 未指定 API 版本時,設定 API 版本為預設的版本 option.AssumeDefaultVersionWhenUnspecified = true; }); } }
擴充套件方法最終實現方式如上面的程式碼所示,之後我們就可以直接在 ConfigureServices 方法中直接進行呼叫這個擴充套件方法就可以了。
// This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { // Config api version services.AddApiVersion(); }
現在我們刪除專案建立時預設生成的 ValuesController,在 Controllers 目錄下建立一個 v1 資料夾,代表此資料夾下都是 v1 版本的控制器。新增一個 UsersController 用來獲取系統的使用者資源,現在專案的檔案結構如下圖所示。
現在我們來改造我們的 UsersController,我們只需要在 Controller 或是 Action 上新增 ApiVersion 特性就可以指定當前 Controller/Action 的版本資訊。同時,因為我需要將 API 的版本資訊新增到生成的 URL 地址中,所以這裡我們需要修改特性路由的模板,將我們的版本以佔位符的形式新增到生成的路由 URL 地址中,修改完成後的程式碼及實現的效果如下所示。
[ApiVersion("1.0")] [ApiController] [Route("api/v{version:apiVersion}/[controller]")] public class UsersController : ControllerBase { }
4、新增對於 Swagger 介面文件的支援
在前後端分離開發的情況下,我們需要提供給前端開發人員一個介面文件,從而讓前端開發人員知道以什麼樣的 HTTP 方法或是傳遞什麼樣的引數給後端介面,從而獲取到正確的資料,而 Swagger 則提供了一種自動生成介面文件的方式,同時也提供類似於 Postman 的功能,可以實現對於介面的實時呼叫測試。
首先,我們需要通過 Nuget 新增 Swashbuckle.AspNetCore 這個 dll 檔案,之後我們就可以在此基礎上實現對於 Swagger 的配置。
Install-Package Swashbuckle.AspNetCore
與上面配置 API 介面的版本資訊相似,這裡我依舊採用構建擴充套件方法的方式來實現對於 Swagger 中介軟體的配置。具體的配置過程可以檢視我之前寫的文章(ASP.NET Core 實戰:構建帶有版本控制的 API 介面),這裡只列出最終配置完成的程式碼。
public static void AddSwagger(this IServiceCollection services) { // 配置 Swagger 文件資訊 services.AddSwaggerGen(s => { // 根據 API 版本資訊生成 API 文件 // var provider = services.BuildServiceProvider().GetRequiredService<IApiVersionDescriptionProvider>(); foreach (var description in provider.ApiVersionDescriptions) { s.SwaggerDoc(description.GroupName, new Info { Contact = new Contact { Name = "Danvic Wang", Email = "danvic96@hotmail.com", Url = "https://yuiter.com" }, Description = "Ingos.API 介面文件", Title = "Ingos.API", Version = description.ApiVersion.ToString() }); } // 在 Swagger 文件顯示的 API 地址中將版本資訊引數替換為實際的版本號 s.DocInclusionPredicate((version, apiDescription) => { if (!version.Equals(apiDescription.GroupName)) return false; var values = apiDescription.RelativePath .Split('/') .Select(v => v.Replace("v{version}", apiDescription.GroupName)); apiDescription.RelativePath = string.Join("/", values); return true; }); // 引數使用駝峰命名方式 s.DescribeAllParametersInCamelCase(); // 取消 API 文件需要輸入版本資訊 s.OperationFilter<RemoveVersionFromParameter>(); // 獲取介面文件描述資訊 var basePath = Path.GetDirectoryName(AppContext.BaseDirectory); var apiPath = Path.Combine(basePath, "Ingos.Api.xml"); s.IncludeXmlComments(apiPath, true); }); }
當我們配置完成後就可以在 Startup 類中去啟用 Swagger 文件。
public void ConfigureServices(IServiceCollection services) { // 新增對於 swagger 文件的支援 services.AddSwagger(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, IApiVersionDescriptionProvider provider) { // 啟用 Swagger 文件 app.UseSwagger(); app.UseSwaggerUI(s => { // 預設載入最新版本的 API 文件 foreach (var description in provider.ApiVersionDescriptions.Reverse()) { s.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", $"Sample API {description.GroupName.ToUpperInvariant()}"); } }); }
因為我們在之前設定構建的 API 路由時包含了版本資訊,所以在最終生成的 Swagger 文件中進行測試時,我們都需要在引數列表中新增 API 版本這個引數。這無疑是有些不方便,所以這裡我們可以通過繼承 IOperationFilter 介面,控制在生成 API 文件時移除 API 版本引數,介面的實現方法如下所示。
public class RemoveVersionFromParameter : IOperationFilter { public void Apply(Operation operation, OperationFilterContext context) { var versionParameter = operation.Parameters.Single(p => p.Name == "version"); operation.Parameters.Remove(versionParameter); } }
當我們實現自定義的介面後就可以在之前針對 Swagger 的擴充套件方法中呼叫這個過濾方法,從而實現移除版本資訊的目的,擴充套件方法中的新增位置如下所示。
public static void AddSwagger(this IServiceCollection services) { // 配置 Swagger 文件資訊 services.AddSwaggerGen(s => { // 取消 API 文件需要輸入版本資訊 s.OperationFilter<RemoveVersionFromParameter>(); }); }
最終的實現效果如下圖所示,可以看到,引數列表中已經沒有版本資訊這個引數,但是我們在進行介面測試時會自動幫我們新增上版本引數資訊。
這裡需要注意,因為我們需要在最終生成的 Swagger 文件中顯示出我們對於 Controller 或是 Action 新增的註釋資訊,所以這裡我們需要在 Web Api 專案的屬性選項中勾選上輸出 XML 文件檔案。同時如果你不想 VS 一直提示你有方法沒有新增引數資訊,這裡我們可以在取消顯示警告這裡新增上 1591 這個引數。
5、構建符合 Restful 風格的介面
在沒有采用 Restful 風格來構建介面返回值時,我們可能會習慣於在介面返回的資訊中新增一個介面是否請求成功的標識,就像下面程式碼中示例的這種返回形式。
{ sueecss: true msg: '', data: [{ id: '20190720214402', name: 'zhangsan' }] }
但是,當我們想要構建符合 Restful 風格的介面時,我們就不能再這樣進行設計了,我們應該通過返回的 HTTP 響應狀態碼來標識這次訪問是否成功。一些比較常用的 HTTP 狀態碼如下表所示。
HTTP 狀態碼 | 涵義 | 解釋說明 |
---|---|---|
200 | OK | 用於一般性的成功返回,不可用於請求錯誤返回 |
201 | Created | 資源被建立 |
202 | Accepted | 用於資源非同步處理的返回,僅表示請求已經收到。對於耗時比較久的處理,一般用非同步處理來完成 |
204 | No Content | 此狀態可能會出現在 PUT、POST、DELETE 的請求中,一般表示資源存在,但訊息體中不會返回任何資源相關的狀態或資訊 |
400 | Bad Request | 用於客戶端一般性錯誤資訊返回, 在其它 4xx 錯誤以外的錯誤,也可以使用,錯誤資訊一般置於 body 中 |
401 | Unauthorized | 介面需要授權訪問,為通過授權驗證 |
403 | Forbidden | 當前的資源被禁止訪問 |
404 | Not Found | 找不到對應的資訊 |
500 | Internal Server Error | 伺服器內部錯誤 |
我們知道 HTTP 共有四個謂詞方法,分別為 Get、Post、Put 和 Delete,在之前我們可能更多的是使用 Get 和 Post,對於 Put 和 Delete 方法可能並不會使用。同樣的,如果我們需要建立符合 Restful 風格的介面,我們則需要根據這四個 HTTP 方法謂詞一些約定俗成的功能定義去定義對應介面的 HTTP 方法。
HTTP 謂詞方法 | 解釋說明 |
---|---|
GET | 獲取資源資訊 |
POST | 提交新的資源資訊 |
PUT | 更新已有的資源資訊 |
DELETE | 刪除資源 |
例如,對於一個獲取所有資源的方法,我們可能會定義介面的預設返回 HTTP 狀態碼為 200 或是 400,當狀態碼為 200 時,代表資料獲取成功,介面可以正常返回資料,當狀態碼為 400 時,則代表介面訪問出現問題,此時則返回錯誤資訊物件。
在 ASP.NET Core Web API 中,我們可以通過在 Action 上新增 ProducesResponseType 特性來定義介面的返回狀態碼。通過 F12 按鍵我們可以進入 ProducesResponseType 這個特性,可以看到這個特性存在兩個構造方法,我們可以只定義介面返回 HTTP 狀態碼或者是在定義介面返回的狀態碼時同時返回的具體物件資訊。
上面給出的介面案例的示例程式碼如下所示,從下圖中可以看到,Swagger 會自動根據我們的 ProducesResponseType 特性來列出我們介面可能返回的 HTTP 狀態碼和物件資訊。這裡因為是示例程式,UserListDto 並沒有定義具體的屬性資訊,所以這裡顯示的是一個不包含任何屬性的物件陣列。
/// <summary> /// 獲取全部的使用者資訊 /// </summary> /// <returns></returns> [HttpGet] [ProducesResponseType(typeof(IEnumerable<UserListDto>), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public IActionResult Get() { // 1、獲取資源資料 // 2、判斷資料獲取是否成功 if (true) return Ok(new List<UserListDto>()); else return BadRequest(new { statusCode = StatusCodes.Status400BadRequest, description = "錯誤描述", msg = "錯誤資訊" }); }
可能這裡你可能會有疑問,當介面返回的 HTTP 狀態碼為 400 時,返回的資訊是什麼鬼,與我們定義的錯誤資訊物件欄位不同啊?原來,在 ASP.NET Core 2.1 之後的版本中,對於 API 介面返回 400 的 HTPP 狀態碼會預設返回 ProblemDetails 物件,因為這裡我們並沒有將介面中的返回 BadRequest 中的錯誤資訊物件作為 ProducesResponseType 特性的建構函式的引數,所以這裡就採用了預設的錯誤資訊物件。
當然,當介面的 HTTP 返回狀態碼為 400 時,最終還是會返回我們自定義的錯誤資訊物件,所以這裡為了不造成前後端對接上的歧義,我們最好將返回的物件資訊也作為引數新增到 ProducesResponseType 特性中。
同時,除了上面示例的介面中通過返回 OK 方法和 BadRequest 方法來表明介面的返回 HTTP 狀態碼,在 ASP.NET Core Web API 中還有下列繼承於 ObjectResult 的方法來表明介面返回的狀態碼,對應資訊如下。
HTTP 狀態碼 | 方法名稱 |
---|---|
200 | OK() |
201 | Created() |
202 | Accepted() |
204 | NoContent() |
400 | BadRequest() |
401 | Unauthorized() |
403 | Forbid() |
404 | NotFound() |
6、使用 Web API 分析器
在上面的示例中,因為我們需要指定介面需要返回的 HTTP 狀態碼,所以我們需要提前新增好 ProducesResponseType 特性,在某些時候我們可能在程式碼中新增了一種 HTTP 狀態碼的返回結果,可是卻忘了新增特性描述,那麼有沒有一種便捷的方式提示我們呢?
在 ASP.NET Core 2.2 及以後更新的 ASP.NET Core 版本中,我們可以通過 Nuget 去新增 Microsoft.AspNetCore.Mvc.Api.Analyze 這個包,從而實現對我們的 API 進行分析,首先我們需要將這個包新增到我們的 API 專案中。
Install-Package Microsoft.AspNetCore.Mvc.Api.Analyzers
例如在下面的介面程式碼中,我們根據使用者的唯一標識去尋找使用者資料,當獲取不到資料的時候,返回的 HTTP 狀態碼為 400,而我們只新增了 HTTP 狀態碼為 200 的特性說明。此時,分析器將 HTTP 404 狀態程式碼的缺失特性說明做為一個警告,並提供了修復此問題的選項,我們進行修復後就可以自動新增特性。
/// <summary> /// 獲取使用者詳細資訊 /// </summary> /// <param name="id">使用者唯一標識</param> /// <returns></returns> [HttpGet("{id}")] [ProducesResponseType(typeof(UserEditDto), StatusCodes.Status200OK)] public IActionResult Get(string id) { // 1、根據 Id 獲取使用者資訊 UserEditDto user = null; if (user == null) return NotFound(); else return Ok(user); }
但是,在自動完成文件補全後其實還是需要我們進行一些操作的,例如,如果我們需要指定返回值的 Type 型別,還是需要我們自己手動新增到 ProducesResponseType 特性上的。
在進行特性補齊的時候,分析器也幫我們填加了一個 ProducesDefaultResponseType 特性。通過在微軟的文件中指向的 Swagger 文件(Swagger Default Response)中可以瞭解到,如果我們介面不管是什麼狀態,最終返回的 response 響應結構都是相同的,我們就可以直接使用 ProducesDefaultResponseType 特性來指定 response 的響應結構,而不需要每個 HTTP 狀態都新增一個特性。
三、總結
在本篇文章中,主要介紹了一些我在使用 ASP.NET Core Web API 的過程中使用到的一些小技巧,以及在以前踩過坑後的一些解決方案,如果對你能有一點的幫助的話,不勝榮幸。同時,如果你有更好的解決方案,或者是針對一些你之前踩過的 Web API 坑的解決方案,也歡迎你在評論區中提出。