.net core的Swagger介面文件使用教程(一):Swashbuckle
現在的開發大部分都是前後端分離的模式了,後端提供介面,前端呼叫介面。後端提供了介面,需要對介面進行測試,之前都是使用瀏覽器開發者工具,或者寫單元測試,再或者直接使用Postman,但是現在這些都已經out了。後端提供了介面,如何跟前端配合說明介面的性質,引數,驗證情況?這也是一個問題。有沒有一種工具可以根據後端的介面自動生成介面文件,說明介面的性質,引數等資訊,又能提供介面呼叫等相關功能呢?
答案是有的。Swagger 是一個規範和完整的框架,用於生成、描述、呼叫和視覺化 RESTful 風格的 Web 服務。而作為.net core開發,Swashbuckle是swagger應用的首選!本文旨在介紹Swashbuckle的一些常見功能,以滿足大部分開發的需要!
本文旨在介紹Swashbuckle的一般用法以及一些常用方法,讓讀者讀完之後對Swashbuckle的用法有個最基本的理解,可滿足絕大部分需求的需要,比如認證問題、虛擬路勁問題,返回值格式問題等等。
如果對Swashbuckle原始碼感興趣,可以去github上pull下來看看
github中Swashbuckle.AspNetCore原始碼地址:https://github.com/domaindrivendev/Swashbuckle.AspNetCore
一、一般用法
建立一個.net core專案(這裡採用的是.net core3.1),然後使用nuget安裝Swashbuckle.AspNetCore,建議安裝5.0以上版本,因為swagger3.0開始已經加入到OpenApi專案中,因此Swashbuckle新舊版本用法還是有一些差異的。
比如,我們一個Home控制器:
/// <summary> /// 測試介面 /// </summary> [ApiController] [Route("[controller]")] public class HomeController : ControllerBase { /// <summary> /// Hello World /// </summary> /// <returns>輸出Hello World</returns>[HttpGet] public string Get() { return "Hello World"; } }
介面修改Startup,在ConfigureServices和Configure方法中新增服務和中介軟體
public void ConfigureServices(IServiceCollection services) {
...
services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo() { Version = "v0.0.1", Title = "swagger測試專案", Description = $"介面文件說明", Contact = new OpenApiContact() { Name = "zhangsan", Email = "[email protected]", Url = null } }); }); ... }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { ... app.UseSwagger(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1"); });
... }
然後執行專案,輸入http://localhost:5000/swagger,得到介面文件頁面:
點選Try it out可以直接呼叫介面。
這裡,發現介面沒有註解說明,這不太友好,而Swashbuckle的介面可以從程式碼註釋中獲取,也可以使用程式碼說明,我們做開發的當然想直接從註釋獲取啦。
但是另一方面,因為註釋在程式碼編譯時會被過濾掉,因此我們需要在專案中生成註釋檔案,然後讓程式載入註釋檔案,操作如下:
右鍵專案=》切換到生成(Build),在最下面輸出輸出中勾選【XML文件檔案】,同時,在錯誤警告的取消顯示警告中新增1591程式碼:
生成當前專案時會將專案中所有的註釋打包到這個檔案中。
然後修改ConfigureServices:
public void ConfigureServices(IServiceCollection services) {
...
services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo() { Version = "v0.0.1", Title = "swagger測試專案", Description = $"介面文件說明", Contact = new OpenApiContact() { Name = "zhangsan", Email = "[email protected]", Url = null } }); options.IncludeXmlComments("SwashbuckleDemo.xml", true); }); ... }
上面使用IncludeXmlComments方法載入註釋,第二個引數true表示註釋檔案包含了控制器的註釋,如果不包含控制器註釋(如引用的其他類庫),可以將它置為false
注意上面的xml檔案要與它對應的dll檔案放到同目錄,如果不在同一目錄,需要自行指定目錄,如果找不到檔案,可能會丟擲異常!。
另外,如果專案引用的其他專案,可以將其他專案也生成xml註釋檔案,然後使用IncludeXmlComments方法載入,從而避免部分介面資訊無註解情況
執行後可以得到介面的註釋:
接著,既然是提供介面,沒有認證怎麼行,比如,Home控制器下還有一個Post介面,但是介面需要認證,比如JwtBearer認證:
/// <summary> /// 測試介面 /// </summary> [ApiController] [Route("[controller]")] public class HomeController : ControllerBase { ... /// <summary> /// 使用認證獲取資料 /// </summary> /// <returns>返回資料</returns> [HttpPost, Authorize] public string Post() { return "這是認證後的資料"; } }
為了介面能使用認證,修改Startup的ConfigureServices:
public void ConfigureServices(IServiceCollection services) {
...
services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo() { Version = "v0.0.1", Title = "swagger測試專案", Description = $"介面文件說明", Contact = new OpenApiContact() { Name = "zhangsan", Email = "[email protected]", Url = null } }); options.IncludeXmlComments("SwashbuckleDemo.xml", true);//第二個引數true表示註釋檔案包含了控制器的註釋 //定義JwtBearer認證方式一 options.AddSecurityDefinition("JwtBearer", new OpenApiSecurityScheme() { Description = "這是方式一(直接在輸入框中輸入認證資訊,不需要在開頭新增Bearer)", Name = "Authorization",//jwt預設的引數名稱 In = ParameterLocation.Header,//jwt預設存放Authorization資訊的位置(請求頭中) Type = SecuritySchemeType.Http, Scheme = "bearer" }); //定義JwtBearer認證方式二 //options.AddSecurityDefinition("JwtBearer", new OpenApiSecurityScheme() //{ // Description = "這是方式二(JWT授權(資料將在請求頭中進行傳輸) 直接在下框中輸入Bearer {token}(注意兩者之間是一個空格))", // Name = "Authorization",//jwt預設的引數名稱 // In = ParameterLocation.Header,//jwt預設存放Authorization資訊的位置(請求頭中) // Type = SecuritySchemeType.ApiKey //}); //宣告一個Scheme,注意下面的Id要和上面AddSecurityDefinition中的引數name一致 var scheme = new OpenApiSecurityScheme() { Reference = new OpenApiReference() { Type = ReferenceType.SecurityScheme, Id = "JwtBearer" } }; //註冊全域性認證(所有的介面都可以使用認證) options.AddSecurityRequirement(new OpenApiSecurityRequirement() { [scheme] = new string[0] }); });
... }
程式執行後效果如下:
上面說了,新增JwtBearer認證有兩種方式,兩種方式的區別如下:
到這裡應該就已經滿足大部分需求的用法了,這也是網上很容易就能搜尋到的,接下來介紹的是一些常用到的方法。
二、服務注入(AddSwaggerGen)
前面介紹到,Swashbuckle的服務注入是在ConfigureServices中使用拓展方法AddSwaggerGen實現的
services.AddSwaggerGen(options => { //使用options注入服務 });
確切的說swagger的服務注入是使用SwaggerGenOptions來實現的,下面主要介紹SwaggerGenOptions的一些常用的方法:
SwaggerDoc
SwaggerDoc主要用來宣告一個文件,上面的例子中聲明瞭一個名稱為v1的介面文件,當然,我們可以宣告多個介面文件,比如按開發版本進行宣告:
options.SwaggerDoc("v1", new OpenApiInfo() { Version = "v0.0.1", Title = "專案v0.0.1", Description = $"介面文件說明v0.0.1", Contact = new OpenApiContact() { Name = "zhangsan", Email = "[email protected]", Url = null } }); options.SwaggerDoc("v2", new OpenApiInfo() { Version = "v0.0.2", Title = "專案v0.0.2", Description = $"介面文件說明v0.0.2", Contact = new OpenApiContact() { Name = "lisi", Email = "[email protected]", Url = null } });
...
開發過程中,可以將介面文件名稱設定成列舉或者常量值,以方便文件名的使用。
宣告多個文件,可以將介面進行歸類,不然一個專案幾百個介面,檢視起來也不方便,而將要介面歸屬某個文件,我們可以使ApiExplorerSettingsAttribute指定GroupName來指定,如:
/// <summary> /// 未使用ApiExplorerSettings特性,表名屬於每一個swagger文件 /// </summary> /// <returns>結果</returns> [HttpGet("All")] public string All() { return "All"; } /// <summary> /// 使用ApiExplorerSettings特性表名該介面屬於swagger文件v1 /// </summary> /// <returns>Get結果</returns> [HttpGet] [ApiExplorerSettings(GroupName = "v1")] public string Get() { return "Get"; } /// <summary> /// 使用ApiExplorerSettings特性表名該介面屬於swagger文件v2 /// </summary> /// <returns>Post結果</returns> [HttpPost] [ApiExplorerSettings(GroupName = "v2")] public string Post() { return "Post"; }
因為我們現在有兩個介面文件了,想要在swaggerUI中看得到,還需要在中介軟體中新增相關檔案的swagger.json檔案的入口:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { ... app.UseSwagger(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1"); options.SwaggerEndpoint("/swagger/v2/swagger.json", "v2"); }); ... }
執行專案後:
上面使用ApiExplorerSettingsAttribute的GroupName屬性指定歸屬的swagger文件(GroupName需要設定成上面SwaggerDoc宣告的文件的名稱),如果不使用ApiExplorerSettingsAttribute,那麼介面將屬於所有的swagger文件,上面的例子可以看到/Home/All介面既屬於v1也屬於v2。
另外ApiExplorerSettingsAttribute還有個IgnoreApi屬性,如果設定成true,將不會在swagger頁面展示該介面。
但是介面一個個的去新增ApiExplorerSettingsAttribute,是不是有點繁瑣了?沒事,我們可以採用Convertion實現,主要是IActionModelConvention和IControllerModelConvention兩個:
IActionModelConvention方式:
public class GroupNameActionModelConvention : IActionModelConvention { public void Apply(ActionModel action) { if (action.Controller.ControllerName == "Home") { if (action.ActionName == "Get") { action.ApiExplorer.GroupName = "v1"; action.ApiExplorer.IsVisible = true; } else if (action.ActionName == "Post") { action.ApiExplorer.GroupName = "v2"; action.ApiExplorer.IsVisible = true; } } } }
然後在ConfigureService中使用:
services.AddControllers(options => { options.Conventions.Add(new GroupNameActionModelConvention()); });
或者使用IControllerModelConvention方式:
public class GroupNameControllerModelConvention : IControllerModelConvention { public void Apply(ControllerModel controller) { if (controller.ControllerName == "Home") { foreach (var action in controller.Actions) { if (action.ActionName == "Get") { action.ApiExplorer.GroupName = "v1"; action.ApiExplorer.IsVisible = true; } else if (action.ActionName == "Post") { action.ApiExplorer.GroupName = "v2"; action.ApiExplorer.IsVisible = true; } } } } }
然後在ConfigureService中使用:
services.AddControllers(options => { options.Conventions.Add(new GroupNameControllerModelConvention()); });
這兩種方式實現的效果和使用ApiExplorerSettingsAttribute是一樣的,細心的朋友可能會注意,action.ApiExplorer.GroupName與ApiExplorerSettingsAttribute.GroupName是對應的,action.ApiExplorer.IsVisible則與ApiExplorerSettingsAttribute.IgnoreApi是對應的
IncludeXmlComments
IncludeXmlComments是用於載入註釋檔案,Swashbuckle會從註釋檔案中去獲取介面的註解,介面引數說明以及介面返回的引數說明等資訊,這個在上面的一般用法中已經介紹了,這裡不再重複說明
IgnoreObsoleteActions
IgnoreObsoleteActions表示過濾掉ObsoleteAttribute屬性宣告的介面,也就是說不會在SwaggerUI中顯示介面了,ObsoleteAttribute修飾的介面表示介面已過期,儘可能不要再使用。
方法呼叫等價於:
options.SwaggerGeneratorOptions.IgnoreObsoleteActions = true;
IgnoreObsoleteProperties
IgnoreObsoleteProperties的作用類似於IgnoreObsoleteActions,只不過IgnoreObsoleteActions是作用於介面,而IgnoreObsoleteProperties作用於介面的請求實體和響應實體引數中的屬性。
方法呼叫等價於:
options.SchemaGeneratorOptions.IgnoreObsoleteProperties = true;
OrderActionsBy
OrderActionsBy用於同一組介面(可以理解為同一控制器下的介面)的排序,預設情況下,一般都是按介面所在類的位置進行排序(原始碼中是按控制器名稱排序,但是同一個控制器中的介面是一樣的)。
比如上面的例子中,我們可以修改成按介面路由長度排序:
options.OrderActionsBy(apiDescription => apiDescription.RelativePath.Length.ToString());
執行後Get介面和Post介面就在All介面前面了:
需要注意的是,OrderActionsBy提供的排序只有升序,其實也就是呼叫IEnumerable<ApiDescription>的OrderBy方法,雖然不理解為什麼只有升序,但降序也是可以採用這個升序實現的,將就著用吧。
TagActionsBy
Tag是標籤組,也就是將介面做分類的一個概念。
TagActionsBy用於獲取一個介面所在的標籤分組,預設的介面標籤分組是控制器名,也就是介面被分在它所屬的控制器下面,我們可以改成按請求方法進行分組
options.TagActionsBy(apiDescription => new string[] { apiDescription.HttpMethod});
執行後:
注意到,上面還有一個Home空標籤,如果不想要這個空標籤,可以將它的註釋去掉,(不明白為什麼Swashbuckle為什麼空標籤也要顯示出來,難道是因為作者想著只要有東西能展示,就應該顯示出來?)
MapType
MapType用於自定義型別結構(Schema)的生成,Schema指的是介面引數和返回值等的結構資訊。
比如,我有一個獲取使用者資訊的介面:
/// <summary> /// 獲取使用者 /// </summary> /// <returns>使用者資訊</returns> [HttpGet("GetUser")] public User GetUser(int id) { //這裡根據Id獲取使用者資訊 return new User() { Name = "張三" }; }
其中User是自己定義的一個實體
/// <summary> /// 使用者資訊 /// </summary> public class User { /// <summary> /// 使用者名稱稱 /// </summary> public string Name { get; set; } /// <summary> /// 使用者密碼 /// </summary> public string Password { get; set; } /// <summary> /// 手機號碼 /// </summary> public string Phone { get; set; } /// <summary> /// 工作 /// </summary> public string Job { get; set; } }
預設情況下,swagger生成的結構是json格式:
通過MapType方法,可以修改User生成的架構,比如修改成字串型別:
options.MapType<User>(() => { return new OpenApiSchema() { Type= "string" }; });
執行後顯示:
AddServer
Server指的是介面訪問的域名和字首(虛擬路徑),以方便訪問不同地址的介面(注意設定跨域).
AddServer用於全域性的新增介面域名和字首(虛擬路徑)部分資訊,預設情況下,如果我們在SwaggerUi頁面使用Try it out去呼叫介面時,預設使用的是當前swaggerUI頁面所在的地址域名資訊:
而AddServer方法執行我們新增其他的地址域名,比如:
options.AddServer(new OpenApiServer() { Url = "http://localhost:5000", Description = "地址1" }); options.AddServer(new OpenApiServer() { Url = "http://127.0.0.1:5001", Description = "地址2" }); //192.168.28.213是我本地IP options.AddServer(new OpenApiServer() { Url = "http://192.168.28.213:5002", Description = "地址3" });
我分別在上面3個埠開啟程式,執行後:
注意:如果讀者本地訪問不到,看看自己程式是否有監聽這三個地址,而且記得要設定跨域,否則會導致請求失敗:
public void ConfigureServices(IServiceCollection services) { ...
services.AddCors();
... }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseCors(builder =>
{
builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
});
...
}
在開發過程中,我們的程式可能會發布到不同的環境,比如本地開發環境,測試環境,預生產環境等等,因此,我們可以使用AddServer方法將不同環境的地址配置上去就能直接實現呼叫了。
在專案部署時,可能會涉及到虛擬目錄之類的東西,比如,使用IIS部署時,可能會給專案加一層虛擬路徑:
或者使用nginx做一層反向代理:
這個時候雖然可以使用http://ip:port/Swashbuckle/swagger/index.html訪問到swaggerUI,但是此時可能會報錯 Not Found /swagger/v1/swagger.json:
這是因為加了虛擬路徑,而swagger並不知道,所以再通過/swagger/v1/swagger.json去獲取介面架構資訊當然會報404了,我們可以改下Swagger中介軟體:
app.UseSwagger(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/Swashbuckle/swagger/v1/swagger.json", "v1"); options.SwaggerEndpoint("/Swashbuckle/swagger/v2/swagger.json", "v2"); });
再使用虛擬路徑就可以訪問到SwaggerUI頁面了,但是問題還是有的,因為所有介面都沒有加虛擬路徑,上面說道,swagger呼叫介面預設是使用SwaggerUI頁面的地址+介面路徑去訪問的,這就會少了虛擬路徑,訪問自然就變成了404:
這個時候就可以呼叫AddServer方法去新增虛擬路徑了:
//注意下面的埠,已經變了
options.AddServer(new OpenApiServer() { Url = "http://localhost:90/Swashbuckle", Description = "地址1" }); options.AddServer(new OpenApiServer() { Url = "http://127.0.0.1:90/Swashbuckle", Description = "地址2" }); //192.168.28.213是我本地IP options.AddServer(new OpenApiServer() { Url = "http://192.168.28.213:90/Swashbuckle", Description = "地址3" });
部署執行後就可以訪問了:
一般的,開發過程中,我們可以把這個虛擬路徑做成配置,在然後從配置讀取即可。
注:我記得Swashbuckle在swagger2.0的版本中SwaggerDocument中有個BasePath,可以很輕鬆的設定虛擬路徑,但是在swagger3+之後把這個屬性刪除了,不知道什麼原因
AddSecurityDefinition
AddSecurityDefinition用於宣告一個安全認證,注意,只是宣告,並未指定介面必須要使用認證,比如宣告JwtBearer認證方式:
//定義JwtBearer認證方式一 options.AddSecurityDefinition("JwtBearer", new OpenApiSecurityScheme() { Description = "這是方式一(直接在輸入框中輸入認證資訊,不需要在開頭新增Bearer)", Name = "Authorization",//jwt預設的引數名稱 In = ParameterLocation.Header,//jwt預設存放Authorization資訊的位置(請求頭中) Type = SecuritySchemeType.Http, Scheme = "bearer" });
AddSecurityDefinition方法需要提供一個認證名以及一個OpenApiSecurityScheme物件,而這個OpenApiSecurityScheme物件就是描述的認證資訊,常用的有:
Type:表示認證方式,有ApiKey,Http,OAuth2,OpenIdConnect四種,其中ApiKey是用的最多的。
Description:認證的描述
Name:攜帶認證資訊的引數名,比如Jwt預設是Authorization
In:表示認證資訊發在Http請求的哪個位置
Scheme:認證主題,只對Type=Http生效,只能是basic和bearer
BearerFormat::Bearer認證的資料格式,預設為Bearer Token(中間有一個空格)
Flows:OAuth認證相關設定,比如認證方式等等
OpenIdConnectUrl:使用OAuth認證和OpenIdConnect認證的配置發現地址
Extensions:認證的其他拓展,如OpenIdConnect的Scope等等
Reference:關聯認證
這些屬性中,最重要的當屬Type,它指明瞭認證的方式,用通俗的話講:
ApiKey表示就是提供一個框,你填值之後呼叫介面,會將填的值與Name屬性指定的值組成一個鍵值對,放在In引數指定的位置通過http傳送到後臺。
Http也是提供了一個框,填值之後呼叫介面,會將填的值按照Scheme指定的方式進行處理,再和Name屬性組成一個鍵值對,放在In引數指定的位置通過http傳送到後臺。這也就解釋了為什麼Bearer認證可以有兩種方式。
OAuth2,OpenIdConnect需要提供賬號等資訊,然後去遠端服務進行授權,一般使用Swagger都不推薦使用這種方式,因為比較複雜,而且授權後的資訊也可以通過ApiKey方式傳送到後臺。
再舉個例子,比如我們使用Cookie認證:
options.AddSecurityDefinition("Cookies", new OpenApiSecurityScheme() { Description = "這是Cookie認證方式", Name = "Cookies",//這個是Cookie名 In = ParameterLocation.Cookie,//資訊儲存在Cookie中 Type = SecuritySchemeType.ApiKey });
注:如果將資訊放在Cookie,那麼在SwaggerUI中呼叫介面時,認證資訊可能不會被攜帶到後臺,因為瀏覽器不允許你自己操作Cookie,因此在傳送請求時會過濾掉你自己設定的Cookie,但是SwaggerUI頁面呼叫生成的Curl命令語句是可以成功訪問的
好了,言歸正傳,當添加了上面JwtBearer認證方式後,這時SwaggerUI多了一個認證的地方:
但是這時呼叫介面並不需要認證資訊,因為還沒有指定哪些介面需要認證資訊
AddSecurityRequirement
AddSecurityDefinition僅僅是宣告已一個認證,不一定要對介面用,而AddSecurityRequirement是將宣告的認證作用於所有介面,比如將上面的JwtBearer認證作用於所有介面:
//宣告一個Scheme,注意下面的Id要和上面AddSecurityDefinition中的引數name一致 var scheme = new OpenApiSecurityScheme() { Reference = new OpenApiReference() { Type = ReferenceType.SecurityScheme, Id = "JwtBearer" } }; //註冊全域性認證(所有的介面都可以使用認證) options.AddSecurityRequirement(new OpenApiSecurityRequirement() { [scheme] = new string[0] });
執行後,發現所有介面後面多了一個鎖,表明此介面需要認證資訊:
AddSecurityRequirement呼叫需要一個OpenApiSecurityRequirement物件,他其實是一個字典型,也就是說可以給介面新增多種認證方式,而它的鍵是OpenApiSecurityScheme物件,比如上面的例子中將新定義的OpenApiSecurityScheme關聯到已經宣告的認證上,而值是一個字串陣列,一般指的是OpenIdConnect的Scope。
需要注意的是,AddSecurityRequirement宣告的作用是對全部的介面生效,也就是說所有介面後面都會加鎖,但這並不影響我們介面的呼叫,畢竟呼叫邏輯還是由後臺程式碼決定的,但是這裡加鎖就容易讓人誤導以為都需要認證。
DocumentFilter
document顧名思義,當然指的就是swagger文件了。
DocumentFilter是文件過濾器,它是在獲取swagger文件介面,返回結果前呼叫,也就是請求swagger.json時呼叫,它允許我們對即將返回的swagger文件資訊做調整,比如上面的例子中新增的全域性認證方式和AddSecurityRequirement新增的效果是一樣的:
public class MyDocumentFilter : IDocumentFilter { public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { //宣告一個Scheme,注意下面的Id要和上面AddSecurityDefinition中的引數name一致 var scheme = new OpenApiSecurityScheme() { Reference = new OpenApiReference() { Type = ReferenceType.SecurityScheme, Id = "JwtBearer" } }; //註冊全域性認證(所有的介面都可以使用認證) swaggerDoc.SecurityRequirements.Add(new OpenApiSecurityRequirement() { [scheme] = new string[0] }); } }
然後使用DocumentFilter方法新增過濾器:
options.DocumentFilter<MyDocumentFilter>();
DocumentFilter方法需要提供一個實現了IDocumentFilter介面的Apply方法的型別和它例項化時所需要的的引數,而IDocumentFilter的Apply方法提供了OpenApiDocument和DocumentFilterContext兩個引數,DocumentFilterContext引數則包含了當前檔案介面方法的資訊,比如呼叫的介面的Action方法和Action的描述(如路由等)。而OpenApiDocument即包含當前請求的介面文件資訊,它包含的屬性全部都是全域性性的, 這樣我們可以像上面新增認證一樣去新增全域性配置,比如,如果不使用AddServer方法,我們可以使用DocumentFilter去新增:
public class MyDocumentFilter : IDocumentFilter { public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { swaggerDoc.Servers.Add(new OpenApiServer() { Url = "http://localhost:90", Description = "地址1" }); swaggerDoc.Servers.Add(new OpenApiServer() { Url = "http://127.0.0.1:90", Description = "地址2" }); //192.168.28.213是我本地IP swaggerDoc.Servers.Add(new OpenApiServer() { Url = "http://192.168.28.213:90", Description = "地址3" }); } }
記得使用DocumentFilter新增過濾器。
再比如,上面我們對介面進行了swagger文件分類使用的是ApiExplorerSettingsAttribute,如果不想對每個介面使用ApiExplorerSettingsAttribute,我們可以使用DocumentFilter來實現,先建立一個類實現IDocumentFilter介面:
public class GroupNameDocumentFilter : IDocumentFilter { string documentName; string[] actions; public GroupNameDocumentFilter(string documentName, params string[] actions) { this.documentName = documentName; this.actions = actions; } public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { foreach (var apiDescription in context.ApiDescriptions) { if (actions.Contains(apiDescription.ActionDescriptor.RouteValues["action"])) { apiDescription.GroupName = documentName; } } } }
然後使用DocumentFilter新增過濾器:
//All和Get介面屬於文件v1 options.DocumentFilter<GroupNameDocumentFilter>(new object[] { "v1", new string[] { nameof(HomeController.Get) } }); //All和Post介面屬於v2 options.DocumentFilter<GroupNameDocumentFilter>(new object[] { "v2", new string[] { nameof(HomeController.Post) } });
然後取消上面Get方法和Post方法的ApiExplorerSettings特性,這樣實現的效果和上面直接使用ApiExplorerSettings特性修飾的效果是相似的。
這裡說相似並非一致,是因為上面的GroupNameDocumentFilter是在第一次獲取swagger.json時執行設定GroupName,也就是說第一次獲取swagger.json會獲取到所有的介面,所以一般也不會採用這種方法,而是採用上面介紹的使用IActionModelConvention和IControllerModelConvention來實現。
OperationFilter
什麼是Operation?Operation可以簡單的理解為一個操作,因為swagger是根據專案中的介面,自動生成介面文件,就自然需要對每個介面進行解析,介面路由是什麼,介面需要什麼引數,介面返回什麼資料等等,而對每個介面的解析就可以視為一個Operation。
OperationFilter是操作過濾器,這個方法需要一個實現類IOperationFilter介面的型別,而它的第二個引數arguments是這個型別例項化時傳入的引數。
OperationFilter允許我們對已經生成的介面進行修改,比如可以新增引數,修改引數型別等等。
需要注意的是,OperationFilter在獲取swagger文件介面時呼叫,也就是請求swagger.json時呼叫,而且只對屬於當前請求介面文件的介面進行過濾呼叫。
比如我們有一個Operation過濾器:
public class MyOperationFilter : IOperationFilter { string documentName; public MyOperationFilter(string documentName) { this.documentName = documentName; } public void Apply(OpenApiOperation operation, OperationFilterContext context) { //過濾處理 } }
接著呼叫SwaggerGenOptions的OperationFilter方法新增
options.OperationFilter<MyOperationFilter>(new object[] { "v1" });
上面的過濾器例項化需要一個引數documentName,所以在OperationFilter方法中有一個引數。
這個介面只會對當前請求的介面文件進行呼叫,也就是說,如果我們請求的是swagger文件v1,也就是請求/swagger/v1/swagger.json時,這個過濾器會對All方法和Get方法執行,如果請求的是swagger文件v2,也就是請求/swagger/v2/swagger.json時,這個過濾器會對All方法和Post方法進行呼叫。自定義的OperationFilter需要實現IOperationFilter的Apply介面方法,而Apply方法有兩個引數:OpenApiOperation和OperationFilterContext,同樣的,OpenApiOperation包含了和當前介面相關的資訊,比如認證情況,所屬的標籤,還可以自定義的自己的Servers。而OperationFilterContext則包換了介面方法的的相關引用。
OperationFilter是用的比較多的方法了,比如上面的全域性認證,因為直接呼叫AddSecurityRequirement新增的是全域性認證,但是專案中可能部分介面不需要認證,這時我們就可以寫一個OperationFilter對每一個介面進行判斷了:
public class ResponsesOperationFilter : IOperationFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { var authAttributes = context.MethodInfo.DeclaringType.GetCustomAttributes(true) .Union(context.MethodInfo.GetCustomAttributes(true)) .OfType<AuthorizeAttribute>(); var list = new List<OpenApiSecurityRequirement>(); if (authAttributes.Any() && !context.MethodInfo.GetCustomAttributes(true).OfType<AllowAnonymousAttribute>().Any()) { operation.Responses["401"] = new OpenApiResponse { Description = "Unauthorized" }; //operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" }); //宣告一個Scheme,注意下面的Id要和AddSecurityDefinition中的引數name一致 var scheme = new OpenApiSecurityScheme() { Reference = new OpenApiReference() { Type = ReferenceType.SecurityScheme, Id = "JwtBearer" } }; //註冊全域性認證(所有的介面都可以使用認證) operation.Security = new List<OpenApiSecurityRequirement>(){new OpenApiSecurityRequirement() { [scheme] = new string[0] }}; } } }
然後使用OperationFilter新增這個過濾器:
options.OperationFilter<ResponsesOperationFilter>();
現在可以測試一下了,我們將上面的All介面使用Authorize特性新增認證
/// <summary> /// 未使用ApiExplorerSettings特性,表名屬於每一個swagger文件 /// </summary> /// <returns>結果</returns> [HttpGet("All"), Authorize] public string All() { return "All"; }
然後執行專案得到:
再比如,我們一般寫介面,都會對返回的資料做一個規範,比如每個介面都會有響應程式碼,響應資訊等等,而程式中我們是通過過濾器去實現的,所以介面都是直接返回資料,但是我們的swagger不知道,比如上面我們的測試介面返回的都是string型別,所以頁面上也是展示string型別沒錯:
假如我們添加了過濾器對結果進行了一個處理,結果不在是string型別了,這個時候我們就可以使用OperationFilter做一個調整了:
public class MyOperationFilter : IOperationFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { foreach (var key in operation.Responses.Keys) { var content = operation.Responses[key].Content; foreach (var mediaTypeKey in content.Keys) { var mediaType = content[mediaTypeKey]; var schema = new OpenApiSchema(); schema.Type = "object"; schema.Properties = new Dictionary<string, OpenApiSchema>() { ["code"] = new OpenApiSchema() { Type = "integer" }, ["message"] = new OpenApiSchema() { Type = "string" }, ["error"] = new OpenApiSchema() { Type = "object", Properties = new Dictionary<string, OpenApiSchema>() { ["message"] = new OpenApiSchema() { Type = "string" }, ["stackTrace"] = new OpenApiSchema() { Type = "string" } } }, ["result"] = mediaType.Schema }; mediaType.Schema = schema; } } } }
記得使用OperationFilter新增過濾器:
options.OperationFilter<MyOperationFilter>();
顯示效果如下:
RequestBodyFilter
RequestBody理所當然的就是請求體了,一般指的就是Post請求,RequestBodyFilter就是允許我們對請求體的資訊作出調整,同樣的,它是在獲取Swagger.json文件時呼叫,而且只對那些有請求體的接口才會執行。
RequestBodyFilter的用法類似DocumentFilter和OperationFilter,一般也不會去修改請求體的預設行為,因為它可能導致請求失敗,所以一般不常用,這裡就不介紹了
ParameterFilter
Parameter指的是介面的引數,而ParameterFilter當然就是允許我們對引數的結構資訊作出調整了,同樣的,它是在獲取Swagger.json文件時呼叫,而且只對那些引數的接口才會執行。
比如,我們有這麼一個介面:
/// <summary> /// 有引數介面 /// </summary> /// <returns></returns> [HttpGet("GetPara")] public string GetPara(string para="default") { return $"para is {para},but para from header is {Request.Headers["para"]}"; }
然後我們可以使用ParameterFilter修改上面para引數在http請求中的位置,比如將它放在請求頭中:
public class MyParameterFilter : IParameterFilter { public void Apply(OpenApiParameter parameter, ParameterFilterContext context) { if (context.ParameterInfo.Name == "para") { parameter.In = ParameterLocation.Header; } } }
然後使用ParameterFilter方法新增過濾器:
options.ParameterFilter<MyParameterFilter>();
執行後:
不過一般不會使用ParameterFilter去修改引數的預設行為,因為這可能會導致介面呼叫失敗。
SchemaFilter
Schema指的是結構,一般指的是介面請求引數和響應返回的引數結構,比如我們想將所有的int型別換成string型別:
public class MySchemaFilter : ISchemaFilter { public void Apply(OpenApiSchema schema, SchemaFilterContext context) { if (context.Type == typeof(int)) { schema.Type = "string"; } } }
加入有介面:
/// <summary> /// 測試介面 /// </summary> /// <returns></returns> [HttpGet("Get")] public int Get(int id) { return 1; }
執行後所有的int引數在swaggerUI上都會顯示為string 型別:
其他方法
其他方法就不準備介紹了,比如:
DescribeAllEnumsAsStrings方法表示在將列舉型別解釋成字串名稱而不是預設的整形數字
DescribeAllParametersInCamelCase方法表示將引數使用駝峰命名法處理
等等這些方法都用的比較少,而且這些都比較簡單,感興趣的可以看看原始碼學習
三、新增Swagger中介軟體(UseSwagger,UseSwaggerUI)
細心地朋友應該注意到,在上面的例子中,新增Swagger中介軟體其實有兩個,分別是UseSwagger和UseSwaggerUI兩個方法:
UseSwagger:新增Swagger中介軟體,主要用於攔截swagger.json請求,從而可以獲取返回所需的介面架構資訊
UseSwaggerUI:新增SwaggerUI中介軟體,主要用於攔截swagger/index.html頁面請求,返回頁面給前端
整個swagger頁面訪問流程如下:
1、瀏覽器輸入swaggerUI頁面地址,比如:http://localhost:5000/swagger/index.html,這個地址是可配置的
2、請求被SwaggerUI中介軟體攔截,然後返回頁面,這個頁面是嵌入的資原始檔,也可以設定成外部自己的頁面檔案(使用外部靜態檔案攔截)
3、頁面接收到Swagger的Index頁面後,會根據SwaggerUI中介軟體中使用SwaggerEndpoint方法設定的文件列表,載入第一個文件,也就是獲取文件架構資訊swagger.json
4、瀏覽器請求的swagger.json被Swagger中介軟體攔截,然後解析屬於請求文件的所有介面,並最終返回一串json格式的資料
5、瀏覽器根據接收到的swagger,json資料呈現UI介面
UseSwagger方法有個包含SwaggerOptions的過載,UseSwaggerUI則有個包含SwaggerUIOptions的過載,兩者相輔相成,所以這裡在一起介紹這兩個方法
SwaggerOptions
SwaggerOptions比較簡單,就三個屬性:
RouteTemplate
路由模板,預設值是/swagger/{documentName}/swagger.json,這個屬性很重要!而且這個屬性中必須包含{documentName}引數。
上面第3、4步驟已經說到,index.html頁面會根據SwaggerUI中介軟體中使用SwaggerEndpoint方法設定的文件列表,然後使用第一個文件的路由傳送一個GET請求,請求會被Swagger中介軟體中攔截,然後Swagger中介軟體中會使用RouteTemplate屬性去匹配請求路徑,然後得到documentName,也就是介面文件名,從而確定要返回哪些介面,所以,這個RouteTemplate一定要配合SwaggerEndpoint中的路由一起使用,要保證通過SwaggerEndpoint方法中的路由能找到documentName。
比如,如果將RouteTemplate設定成:
app.UseSwagger(options => { options.RouteTemplate = "/{documentName}.json"; });
那麼SwaggerEndpoint就得做出相應的調整:
app.UseSwaggerUI(options => { options.SwaggerEndpoint("/v1.json", "v1"); options.SwaggerEndpoint("/v2.json", "v2"); });
當然,上面的SwaggerEndpoint方法中的路由可以新增虛擬路徑,畢竟虛擬路徑會在轉發時被處理掉。
總之,這個屬性很重要,儘可能不要修改,然後是上面預設的格式在SwaggerEndpoint方法中宣告。
SerializeAsV2
表示按Swagger2.0格式序列化生成swagger.json,這個不推薦使用,儘可能的使用新版本的就可以了。
PreSerializeFilters
這個屬性也是個過濾器,類似於上面介紹的DocumentFilter,在解析完所有介面後得到swaggerDocument之後呼叫執行,也就是在DocumentFilter,OperationFilter等過濾器之後呼叫執行。不建議使用這個屬性,因為它能實現的功能使用DocumentFilter,OperationFilter等過濾器都能實現。
SwaggerUIOptions
SwaggerUIOptions則包含了SwaggerUI頁面的一些設定,主要有六個屬性:
RoutePrefix
設定SwaggerUI的Index頁面的地址,預設是swagger,也就是說可以使用http://host:port/swagger可以訪問到SwaggerUI頁面,如果設定成空字串,那麼久可以使用http://host:port直接訪問到SwaggerUI頁面了
IndexStream
上面解釋過,Swagger的UI頁面是嵌入的資原始檔,預設值是:
app.UseSwaggerUI(options => { options.IndexStream = () => typeof(SwaggerUIOptions).GetTypeInfo().Assembly.GetManifestResourceStream("Swashbuckle.AspNetCore.SwaggerUI.index.html"); });
我們可以修改成自己的頁面,比如Hello World:
app.UseSwaggerUI(options => { options.IndexStream = () => new MemoryStream(Encoding.UTF8.GetBytes("Hello World")); });
DocumentTitle
這個其實就是html頁面的title
HeadContent
這個屬性是往SwaggerUI頁面head標籤中新增我們自己的程式碼,比如引入一些樣式檔案,或者執行自己的一些指令碼程式碼,比如:
app.UseSwaggerUI(options => { options.HeadContent += $"<script type='text/javascript'>alert('歡迎來到SwaggerUI頁面')</script>"; });
然後進入SwaggerUI就會彈出警告框了。
注意,上面的設定使用的是+=,而不是直接賦值。
但是一般時候,我們不是直接使用HeadConten屬性的,而是使用 SwaggerUIOptions的兩個拓展方法去實現:InjectStylesheet和InjectJavascript,這兩個拓展方法主要是注入樣式和javascript程式碼:
/// <summary> /// Injects additional CSS stylesheets into the index.html page /// </summary> /// <param name="options"></param> /// <param name="path">A path to the stylesheet - i.e. the link "href" attribute</param> /// <param name="media">The target media - i.e. the link "media" attribute</param> public static void InjectStylesheet(this SwaggerUIOptions options, string path, string media = "screen") { var builder = new StringBuilder(options.HeadContent); builder.AppendLine($"<link href='{path}' rel='stylesheet' media='{media}' type='text/css' />"); options.HeadContent = builder.ToString(); } /// <summary> /// Injects additional Javascript files into the index.html page /// </summary> /// <param name="options"></param> /// <param name="path">A path to the javascript - i.e. the script "src" attribute</param> /// <param name="type">The script type - i.e. the script "type" attribute</param> public static void InjectJavascript(this SwaggerUIOptions options, string path, string type = "text/javascript") { var builder = new StringBuilder(options.HeadContent); builder.AppendLine($"<script src='{path}' type='{type}'></script>"); options.HeadContent = builder.ToString(); }
ConfigObject
其他配置物件,包括之前介紹的SwaggerDocument文件的地址等等。
OAuthConfigObject
和OAuth認證有關的配置資訊,比如ClientId、ClientSecret等等。
對於ConfigObject,OAuthConfigObject兩個物件,一般都不是直接使用它,而是用SwaggerUIOptions的拓展方法,比如之前一直介紹的SwaggerEndpoint方法,其實就是給ConfigObject的Urls屬性增加物件:
/// <summary> /// Adds Swagger JSON endpoints. Can be fully-qualified or relative to the UI page /// </summary> /// <param name="options"></param> /// <param name="url">Can be fully qualified or relative to the current host</param> /// <param name="name">The description that appears in the document selector drop-down</param> public static void SwaggerEndpoint(this SwaggerUIOptions options, string url, string name) { var urls = new List<UrlDescriptor>(options.ConfigObject.Urls ?? Enumerable.Empty<UrlDescriptor>()); urls.Add(new UrlDescriptor { Url = url, Name = name} ); options.ConfigObject.Urls = urls; }
四、總結
到這裡基本上就差不多了,寫了這麼多該收尾了。
主要就是記住三點:
1、服務注入使用AddSwaggerGen方法,主要就是生成介面相關資訊,如認證,介面註釋等等,還有幾種過濾器幫助我們實現自己的需求
2、中介軟體注入有兩個:UseSwagger和UseSwaggerUI:
UseSwagger負責返回介面架構資訊,返回的是json格式的資料
UseSwaggerUI負責返回的是頁面資訊,返回的是html內容
3、如果涉及到介面生成的,儘可能在AddSwaggerGen中實現,如果涉及到UI頁面的,儘可能在UseSwaggerUI中實現