1. 程式人生 > 實用技巧 >.net core的Swagger介面文件使用教程(一):Swashbuckle

.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中實現