一、前言 在目前的軟體開發的潮流中,不管是前後端分離還是服務化改造,後端更多的是通過構建 API 介面服務從而為 web、app、desktop 等各種客戶端提供業務支持,如何構建一個符合規範、容易理解的 API 介面是我們後端開發人員需要考慮的。在本篇文章中,我將列舉一些我在使用 ASP.NET ...
一、前言
在目前的軟體開發的潮流中,不管是前後端分離還是服務化改造,後端更多的是通過構建 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 = "[email protected]", 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 坑的解決方案,也歡迎你在評論區中提出。
我的博客即將同步至騰訊雲+社區,邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=1jarnly4f8ua3