現在的開發大部分都是前后端分離的模式了,后端提供接口,前端調用接口。后端提供了接口,需要對接口進行測試,之前都是使用瀏覽器開發者工具,或者寫單元測試,再或者直接使用Postman,但是現在這些都已經out了。后端提供了接口,如何跟前端配合說明接口的性質,參數,驗證情況?這也是一個問題。有沒有一種工具可以根據后端的接口自動生成接口文檔,說明接口的性質,參數等信息,又能提供接口調用等相關功能呢?
答案是有的。Swagger 是一個規范和完整的框架,用於生成、描述、調用和可視化 RESTful 風格的 Web 服務。而作為.net core開發,Swashbuckle是swagger應用的首選!本文旨在介紹Swashbuckle的一些常見功能,以滿足大部分開發的需要!
本文旨在介紹Swashbuckle的一般用法以及一些常用方法,讓讀者讀完之后對Swashbuckle的用法有個最基本的理解,可滿足絕大部分需求的需要,比如認證問題、虛擬路勁問題,返回值格式問題等等。
如果對Swashbuckle源碼感興趣,可以去github上pull下來看看
github中Swashbuckle.AspNetCore源碼地址:https://github.com/domaindrivendev/Swashbuckle.AspNetCore
一、一般用法
注:這里一般用法的Demo源碼已上傳到百度雲:https://pan.baidu.com/s/1Z4Z9H9nto_CbNiAZIxpFFQ (提取碼:pa8s ),下面第二、三部分的功能可在Demo源碼基礎上去嘗試。
創建一個.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 = "xxx@qq.com", 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代碼:
注:建議這里添加1591,因為如果不添加,而且勾選【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 = "xxx@qq.com", 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 = "xxx@qq.com", 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 = "xxx@qq.com", 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 = "xxxx@qq.com", Url = null } });
...
開發過程中,可以將接口文檔名稱設置成枚舉或者常量值,以方便文檔名的使用。
至於上面OpenApiInfo聲明的各參數,其實就是要在SwaggerUI頁面上展示出來的,讀者可自行測試一下,這里不過多說明,只是順帶提一下Description屬性,這個是一個介紹文檔接口的簡介,但是這個屬性是支持html展示的,也就是說可以生成一些html代碼放到Description屬性中。
聲明多個文檔,可以將接口進行歸類,不然一個項目幾百個接口,查看起來也不方便,而將要接口歸屬某個文檔,我們可以使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方法,雖然不理解為什么只有升序,但降序也是可以采用這個升序實現的,將就着用吧。
CustomSchemaIds
CustomSchemaIds方法用於自定義SchemaId,Swashbuckle中的每個Schema都有唯一的Id,框架會使用這個Id匹配引用類型,因此這個Id不能重復。
默認情況下,這個Id是根據類名得到的(不包含命名空間),因此,當我們有兩個相同名稱的類時,Swashbuckle就會報錯:
System.InvalidOperationException: Can't use schemaId "$XXXXX" for type "$XXXX.XXXX". The same schemaId is already used for type "$XXXX.XXXX.XXXX"
就是類似上面的異常,一般時候我們都得去改類名,有點不爽,這時就可以使用這個方法自己自定義實現SchemaId的獲取,比如,我們自定義實現使用類名的全限定名(包含命名空間)來生成SchemaId,上面的異常就沒有了:
options.CustomSchemaIds(CustomSchemaIdSelector); string CustomSchemaIdSelector(Type modelType) { if (!modelType.IsConstructedGenericType) return modelType.FullName.Replace("[]", "Array"); var prefix = modelType.GetGenericArguments() .Select(genericArg => CustomSchemaIdSelector(genericArg)) .Aggregate((previous, current) => previous + current); return prefix + modelType.FullName.Split('`').First(); }
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是將聲明的認證作用於所有接口(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 類型:
再比如,我們可以使用SchemaFilter來處理枚舉類型注釋的顯示問題,舉個例子:
比如我們有一個性別枚舉類型:
public enum SexEnum { /// <summary> /// 未知 /// </summary> Unknown = 0, /// <summary> /// 男 /// </summary> Male = 1, /// <summary> /// 女 /// </summary> Female = 2 }
然后有個User類持有此枚舉類型的一個屬性:
public class User { /// <summary> /// 用戶Id /// </summary> public int Id { get; set; } /// <summary> /// 用戶名稱 /// </summary> public string Name { get; set; } /// <summary> /// 用戶性別 /// </summary> public SexEnum Sex { get; set; } }
如果將User類作為接口參數或者返回類型,比如有下面的接口:
/// <summary> /// 獲取一個用戶信息 /// </summary> /// <param name="userId">用戶ID</param> /// <returns>用戶信息</returns> [HttpGet("GetUserById")] public User GetUserById(int userId) { return new User(); }
直接運行后得到的返回類型的說明是這樣的:
這就有個問題了,枚舉類型中的0、1、2等等就是何含義,這個沒有在swagger中體現出來,這個時候我們可以通過SchemaFilter來修改Schema信息。
比如,可以先用一個特性(例如使用DescriptionAttribute)標識枚舉類型的每一項,用於說明含義:
public enum SexEnum { /// <summary> /// 未知 /// </summary> [Description("未知")] Unknown = 0, /// <summary> /// 男 /// </summary> [Description("男")] Male = 1, /// <summary> /// 女 /// </summary> [Description("女")] Female = 2 }
接着我們創建一個MySchemaFilter類,實現ISchemaFilter接口:

public class MySchemaFilter : ISchemaFilter { static readonly ConcurrentDictionary<Type, Tuple<string, object>[]> dict = new ConcurrentDictionary<Type, Tuple<string, object>[]>(); public void Apply(OpenApiSchema schema, SchemaFilterContext context) { if (context.Type.IsEnum) { var items = GetTextValueItems(context.Type); if (items.Length > 0) { string decription = string.Join(",", items.Select(f => $"{f.Item1}={f.Item2}")); schema.Description = string.IsNullOrEmpty(schema.Description) ? decription : $"{schema.Description}:{decription}"; } } else if (context.Type.IsClass && context.Type != typeof(string)) { UpdateSchemaDescription(schema, context); } } private void UpdateSchemaDescription(OpenApiSchema schema, SchemaFilterContext context) { if (schema.Reference!=null) { var s = context.SchemaRepository.Schemas[schema.Reference.Id]; if (s != null && s.Enum != null && s.Enum.Count > 0) { if (!string.IsNullOrEmpty(s.Description)) { string description = $"【{s.Description}】"; if (string.IsNullOrEmpty(schema.Description) || !schema.Description.EndsWith(description)) { schema.Description += description; } } } } foreach (var key in schema.Properties.Keys) { var s = schema.Properties[key]; UpdateSchemaDescription(s, context); } } /// <summary> /// 獲取枚舉值+描述 /// </summary> /// <param name="enumType"></param> /// <returns></returns> private Tuple<string, object>[] GetTextValueItems(Type enumType) { Tuple<string, object>[] tuples; if (dict.TryGetValue(enumType, out tuples) && tuples != null) { return tuples; } FieldInfo[] fields = enumType.GetFields(); List<KeyValuePair<string, int>> list = new List<KeyValuePair<string, int>>(); foreach (FieldInfo field in fields) { if (field.FieldType.IsEnum) { var attribute = field.GetCustomAttribute<DescriptionAttribute>(); if (attribute == null) { continue; } string key = attribute?.Description ?? field.Name; int value = ((int)enumType.InvokeMember(field.Name, BindingFlags.GetField, null, null, null)); if (string.IsNullOrEmpty(key)) { continue; } list.Add(new KeyValuePair<string, int>(key, value)); } } tuples = list.OrderBy(f => f.Value).Select(f => new Tuple<string, object>(f.Key, f.Value.ToString())).ToArray(); dict.TryAdd(enumType, tuples); return tuples; } }
最后在Startup中使用
services.AddSwaggerGen(options => { ... options.SchemaFilter<MySchemaFilter>(); });
再次運行項目后,得到的架構就有每個枚舉項的屬性了,當然,你也可以安裝自己的意願去生成特定格式的架構,這只是一個簡單的例子
其他方法
其他方法就不准備介紹了,比如:
DescribeAllEnumsAsStrings方法表示在將枚舉類型解釋成字符串名稱而不是默認的整形數字
DescribeAllParametersInCamelCase方法表示將參數使用駝峰命名法處理
等等這些方法都用的比較少,而且這些都比較簡單,感興趣的可以看看源碼學習
另外需要注意的是,在Swashbuckle.AspNetCore 6.0+以后的版本中,上面兩個方法已經被移除了,作者希望我們通過.net core提供的依賴注入及JsonConverter機制自行去實現。
但是作者有提供了一個 Swashbuckle.AspNetCore.Newtonsoft 包,基於Newtonsoft.Json 來實現DescribeAllEnumsAsStrings,DescribeAllParametersInCamelCase 原來的這兩個方法:
services.AddSwaggerGenNewtonsoftSupport(); services.Configure<MvcNewtonsoftJsonOptions>(options => { //等價於原來的DescribeAllEnumsAsStrings方法 options.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter()); //等價於原來的DescribeAllParametersInCamelCase方法 options.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter(new Newtonsoft.Json.Serialization.CamelCaseNamingStrategy())); });
特別注意的是,這樣做是解決Swagger頁面展示枚舉類型時按字符串展示,但真實調用接口返回的格式還是需要自行實現JsonConverter。
畢竟Swagger只是接口說明文檔,它不影響真實接口返回的數據信息,而.net core的MVC序列化有兩種方案:Newtonsoft.Json和System.Text.Json,所以這也是預料之中的事。
三、添加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中實現