在目前的軟件開發的潮流中,不論是先後端分離仍是服務化改造,後端更多的是經過構建 API 接口服務從而爲 web、app、desktop 等各類客戶端提供業務支持,如何構建一個符合規範、容易理解的 API 接口是咱們後端開發人員須要考慮的。在本篇文章中,我將列舉一些我在使用 ASP.NET Core Web API 構建接口服務時使用到的一些小技巧,因才疏學淺,可能會存在不對的地方,歡迎指出。css
代碼倉儲:https://github.com/Lanesra712...html
由於本篇文章中涉及到的一些知識點在以前的文章中也已經有具體的解釋了,因此這裏只會說明如何在 ASP.NET Core Web API 中如何去使用,不會作過多的詳細介紹。若是你須要詳細瞭解的話,能夠跳轉到文章中給出的外鏈地址去查看。前端
本篇文章中使用的代碼是基於 .NET Core 2.2 + .NET Standard 2.0 進行構建的,若是你採用的版本與我使用的不一樣,可能最終實現起來的代碼會有所不一樣,請提早知悉。同時,本篇文章中全部示例代碼都會存在於前言中所列出的 github repo 中,我會嘗試將每一個功能點的開發做爲一次 commit,而且也會在後續進行不按期的更新完善,最終搭建一個基於領域驅動思想的後端項目模板,若是對你有幫助的話,歡迎持續關注。nginx
在我以前的一篇文章中(構建可讀性更高的 ASP.NET Core 路由)有提到過,由於 .NET 默認採用 Pascal 的類命名方式,若是採用默認生成的路由,最終構建出的路由地址會存在大小寫混在一塊兒的狀況,雖然在 .NET Core 中大小寫的路由地址最終都會對於到正確的資源上,可是爲了更好的符合前端的規範,因此這裏咱們首先按照以前的文章中所列出的方法去修改默認生成的路由地址格式。git
由於這裏咱們最終想要實現的是符合 Restful 風格的 API 接口,因此這裏咱們首先須要將默認生成的 URL 地址改成全小寫模式。github
public void ConfigureServices(IServiceCollection services) { // 採用小寫的 URL 路由模式 services.AddRouting(options => { options.LowercaseUrls = true; }); }
若是你有看過構建可讀性更高的 ASP.NET Core 路由這篇文章,你會發現其實咱們最終實現的是 hyphen(-) 格式的 Url 地址,那麼這裏咱們爲何不進行後續的修改了呢?web
若是你有查看 .NET Core 默認模板中生成的 API Controller,仔細看下,這裏實際上是使用的特性路由,因此這裏咱們並不能經過 Startup.UseMvc 定義的傳統路由模板,或是直接在 Startup.Configure 中的 UseMvcWithDefaultRoute 方法去修改咱們的生成的路由地址格式。json
[Route("api/[controller]")] [ApiController] public class ValuesController : ControllerBase { }
不論是後端接口的服務化改造,仍是隻是單純的先後端分離項目開發,咱們的前端項目與後端接口一般不會部署在一塊兒,因此咱們須要解決前端訪問接口時會涉及到的跨域訪問的問題。後端
針對跨域請求,咱們能夠採用 jsonp、或者是經過給 nginx 服務器配置響應的 header 參數頭信息、或者是使用 CORS,又或是其它的解決方案。你能夠自由選擇,這裏我採用在後端接口中直接配置對於 CORS 的支持。api
在 .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": "*"
}
在一些涉及到接口功能升級的場景下,當咱們須要修改接口邏輯而舊版本的接口沒法停用的狀況時,爲了減小對於原有接口的影響,咱們能夠採起爲接口添加版本信息的形式,從而下降因採用不一樣版本而形成的影響。若是你想要詳細瞭解的話,能夠查看這篇文章,電梯直達 =》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 { }
在先後端分離開發的狀況下,咱們須要提供給前端開發人員一個接口文檔,從而讓前端開發人員知道以什麼樣的 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 這個參數。
在沒有采用 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() { // 一、獲取資源數據 // 二、判斷數據獲取是否成功 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() |
在上面的示例中,由於咱們須要指定接口須要返回的 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) { // 一、根據 Id 獲取用戶信息 UserEditDto user = null; if (user == null) return NotFound(); else return Ok(user); }
可是,在自動完成文檔補全後其實仍是須要咱們進行一些操做的,例如,若是咱們須要指定返回值的 Type 類型,仍是須要咱們本身手動添加到 ProducesResponseType 特性上的。
在進行特性補齊的時候,分析器也幫咱們填加了一個 ProducesDefaultResponseType 特性。經過在微軟的文檔中指向的 Swagger 文檔(Swagger Default Response)中能夠了解到,若是咱們接口不論是什麼狀態,最終返回的 response 響應結構都是相同的,咱們就能夠直接使用 ProducesDefaultResponseType 特性來指定 response 的響應結構,而不須要每一個 HTTP 狀態都添加一個特性。