淺析如何在Nancy中使用Swagger生成API文檔

前言

上一篇博客介紹了使用Nancy框架內部的方法來建立了一個簡單到不能再簡單的Document。可是還有許許多多的不足。html

爲了能稍微完善一下這個Document,這篇引用了當前流行的Swagger,以及另外一個開源的Nancy.Swagger項目來完成今天的任務!git

注:Swagger是已經相對成熟的了,但Nancy(2.0.0-clinteastwood)和Nancy.Swagger(2.2.6-alpha)是基於目前的最新版本,但目前的都是沒有發佈正式版,因此後續API可能會有些許變化。github

下面先來簡單看看什麼是Swaggerjson

何爲Swagger

The World's Most Popular Framework for APIs.這是Swagger官方的描述。能說出是世界上最流行的,也是要有必定資本的!c#

光看這個描述就知道Swagger不會差!畢竟人家敢這樣說。固然我的也認爲Swagger確實很不錯。windows

經過官方文檔,咱們都知道要想生成Swagger文檔,可使用YAML或JSON兩種方式來書寫,因爲咱們日常寫程序用的比較多的是JSON!api

因此本文主要是使用了JSON,順帶說一下YAML的語法也是屬於易懂易學的。數組

既然是用JSON書寫,那麼要怎麼寫呢?這個實際上是有一套規定、約束,咱們只要遵照這些來寫就能夠了。詳細內容能夠參見OpenAPI Specification安全

本文後面的內容將默認園友們對Swagger有過了解。服務器

Swagger主要有下面幾個東西,要引用基本的樣式和腳本就不在多說了。

固然,引用樣式和腳本只是最基本的前提,下面這段js(來自swagger-ui項目)纔是最爲主要的!

<script>
window.onload = function() {
    // Build a system
    const ui = SwaggerUIBundle({
        url: "your url",//返回json數據的url地址
        dom_id: '#swagger-ui',//在這個div展現內容
        presets: [
            SwaggerUIBundle.presets.apis,
            SwaggerUIStandalonePreset
        ],
        plugins: [
            SwaggerUIBundle.plugins.DownloadUrl
        ],
        layout: "StandaloneLayout"
    })

    window.ui = ui
}
</script>

就是在上面加上註釋的兩個屬性:url指定了咱們要展現數據(JSON格式)的來源,dom_id指定了在id爲swagger-ui的容器中展現咱們的文檔。

在加載的時候建立了Swagger相關的內容,主要的有下面的兩個,其他的用默認的就能夠了。

簡單來講,咱們請求了這個url拿到了這些json數據,再根據這些數據在dom_id中構造出咱們所看到的頁面。有那麼點數據驅動的意思。

固然這些JSON數據是有格式要求的。能夠看看下面的簡單示例

{
  "swagger": "2.0",
  "info": {
    "title": "Simple API overview",
    "version": "v2"
  },
  "paths": {
    "/": {
      "get": {
        "operationId": "listVersionsv2",
        "summary": "List API versions",
        "produces": [
          "application/json"
        ],
        "responses": {
          "200": {
            "description": "200 300 response",
            "examples": {
              "application/json": "一串json"
            }
          }
        }
      }
    }
  },
  "consumes": [
    "application/json"
  ]
}

這也就意味着咱們只須要嚴格按照Swagger的定義,就能夠生成一個即美觀,又可執行的API文檔了。

更多相關JSON示例可參見

https://github.com/OAI/OpenAPI-Specification/tree/master/examples/v2.0/json

Nancy.Swagger說明

Nancy.Swagger是咱們今天的主角,是一個基於MIT協議的開源項目。Github地址:Nancy.Swagger

固然經過上面關於Swagger的說明,也已經大概明白了這個項目主要爲咱們作了什麼。就是構造Swgger所須要的JSON格式的數據!

它並無像Swashbuckle.AspNetCore同樣集成了SwaggerUI的內容到項目中去,只是一個提供數據的項目。

其官方的示例Demo是用跳轉到petstore.swagger.io方式來完成的。可是常常性是要等待很長時間的,應該是網絡的問題。

爲了不這一狀況,能夠經過下面的操做避免:

  • 手動下載swagger-ui相關的內容並添加到咱們的新項目中。同時我還將這些設置成嵌入式的資源。

image

  • 添加一個用於顯示的頁面,示例爲doc.html,內容能夠照搬swagger-ui目錄下面的index.html

  • 在Bootstrapper中添加靜態資源的引用

protected override void ConfigureConventions(NancyConventions nancyConventions)
{
    base.ConfigureConventions(nancyConventions);
    nancyConventions.StaticContentsConventions.Add(StaticContentConventionBuilder.AddDirectory("swagger-ui"));
}
  • 在訪問咱們API時,將其重定向到doc.html頁面
public class HomeModule : NancyModule
{
    public HomeModule()
    {
        Get("/", _ =>
        {
            return Response.AsRedirect("/swagger-ui");     
        });

        Get("/swagger-ui",_=>
        {                            
            var url = $"{Request.Url.BasePath}/api-docs";
            return View["doc", url];
        });
    }
}
  • 修改doc.html的內容,將上述的url,替換成@Model
window.onload = function() {
// Build a system
const ui = SwaggerUIBundle({
    url: "@Model",
    dom_id: '#swagger-ui',
    presets: [
        SwaggerUIBundle.presets.apis,
        SwaggerUIStandalonePreset
    ],
    plugins: [
        SwaggerUIBundle.plugins.DownloadUrl
    ],
    layout: "StandaloneLayout"
})
window.ui = ui
}

完成上面的內容後,就開始構造咱們的文檔了。

構造文檔的基本信息

這裏主要是設置這個API文檔的概要信息,好比文檔的標題,此api的版本等

須要經過SwaggerMetadataProvider的SetInfo方法來設置這些信息

下面是具體的示例代碼,寫在Bootstrapper中:

protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
{
    SwaggerMetadataProvider.SetInfo("Nancy Swagger Example", "v1.0", "Some open api", new Contact()
    {
        EmailAddress = "catcher_hwq@outlook.com",
        Name = "Catcher Wong",
        Url = "http://www.cnblogs.com/catcher1994"
    }, "http://www.cnblogs.com/catcher1994");

    base.ApplicationStartup(container, pipelines);
}

此時對應的大體效果(這個時候是不能正常運行的,只是顯示了這部分的效果)以下:

image

上面代碼生成的JSON數據是符合規範的,以下所示:

image

下面要作的就是構造路由相關的信息

不帶任何請求參數

先在Module中定義一個簡單的路由,這個路由不帶任何參數。

Get("/", _ =>
{
    var list = new List<Product>
    {
        new Product{ Name="p1", Price=199 , IsActive = true },
        new Product{ Name="p2", Price=299 , IsActive= true }
    };

    return Negotiate.WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/json"), list);                
}, null, "GetProductList");

而後在MetadataModule中添加相應的描述,這裏的MetadataModule與上一篇是類似的,這也是爲何我會在上一篇先介紹不使用

第三方組件的來構造的緣由,由於這種寫法下面,二者沒有本質的區別!

Describe["GetProductList"] = desc => desc.AsSwagger(
    with => with.Operation(
        op => op.OperationId("GetProductList")
        .Tag("Products")
        .Summary("Get all products")
        .Response(r=>r.Schema<IEnumerable<Product>>().Description("OK"))
        .Description("This returns a list of products")
        ));

下面是部分Nancy.Swagger裏面的核心內容,也是上一篇所沒有的特殊之處。

AsSwagger是RouteDescription一個擴展方法,這個方法是返回咱們須要的PathItem。

OperationId是這個路由的一個友好名稱,源碼裏面的字段定義代表它要惟一。對更加詳盡的描述可能去看Swagger中對這些參數的說明!

Tag能夠理解爲這個路由屬於那個分組,起分隔符的做用,舉個例子,如今有A,B兩個模塊的API,咱們確定不能把它們交叉排列下去

而是A的放到一個地方,B的一個地方,便於咱們的的區分。

Summary是當前路由的精簡描述,要小於120個字符。

Description是當前路由的詳細描述。

Response是指望的運行結果的相關內容,能夠有多個,這裏沒有標明狀態碼,而是直接寫處理的內容,此時說明這裏用的是默認的狀態碼。

Response裏面又是一個委託,裏面又有部分定義:

Schema 代表當前響應應該返回的類型是什麼

Description是這個響應對應的描述信息

這個時候是會出錯的,由於咱們在Respoonse的時候指定了Schema,可是咱們並無指定它的定義。

咱們須要先在MetadataModule中引用ISwaggerModelCatalog這個接口並調用它的AddModel方法把相關的類型添加進去,這樣才能正常運行!

public ProductsMetadataModule(ISwaggerModelCatalog modelCatalog)
{
    //添加相應的類型
    modelCatalog.AddModels(typeof(Product), typeof(IEnumerable<Product>));
    
     Describe["GetProductList"] = desc => desc.AsSwagger(
        with => with.Operation(
            op =>
            op.OperationId("GetProductList")
            .Tag("Products")
            .Summary("Get all products")
            //在Schema中使用modelCatalog
            .Response(r => r.Schema<IEnumerable<Product>>(modelCatalog).Description("OK"))
            .Description("This returns a list of products")
            ));
}

示例結果以下:

先來看看上面設置對應的內容:

image

點擊Try it out運行的結果

image

能夠看到使用curl 去訪問咱們的實際接口拿到服務器的響應信息(結果和頭部)

在終端執行一下這個命令,也是這個結果。

image

帶Path參數和Query參數

一樣的,先在Module中定義一個路由,這個路由包含了一個Path參數和一個Query參數

Get("/{productid}", _ =>
{

    var productId = _.productid;

    if (string.IsNullOrWhiteSpace(productId))
    {
        return HttpStatusCode.NotFound;
    }

    var isActive = Request.Query.isActive ?? true;

    var product = new Product
    {
        Name = "apple",
        Price = 100,
        IsActive = isActive
    };

    return Negotiate.WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/json"), product);
}, null, "GetProductByProductId");

這裏做了多一點操做,爲的是演示儘量多的用法。若是傳遞的產品id爲空,則直接返回404。若是沒有輸入isActive這個Query參數

返回Productr的IsActive就爲false。

而後在MetadataModule中添加相應的描述

Describe["GetProductByProductId"] = desc => desc.AsSwagger(
        with => with.Operation(
            op => op.OperationId("GetProductByProductId")
            .Tag("Products2")
            .Summary("Get a product by product's id")
            .Description("This returns a product's infomation by the special id")
            .Parameter(new Parameter
            {
                Name = "productid",
                In = ParameterIn.Path,//指明該參數是對應路由上面的同名參數
                Required = true,//必填
                Description = "id of a product"
            })
            .Parameter(new Parameter
            {
                Name = "isactive",
                In = ParameterIn.Query,//指明該參數是對應QueryString上面的參數
                Description = "get the actived product",
                Required = false//非必填
            })
            .Response(r => r.Schema<Product>(modelCatalog).Description("Here is the product"))
            .Response(404, r => r.Description("Can't find the product"))
            ));

這裏多了一個Parameter是上面沒有提到的,這個就是咱們的請求參數,這裏的請求參數包含下面五種:

  • Path
  • Query
  • Body
  • Header
  • Form

下面是運行的效果圖,分別演示了下面幾種狀況

  • 不填productid,不能執行,輸入框會變紅
  • 填了productid,能執行,可是服務器端返回的isactive是false
  • 填了productid和isactive,能執行,服務器返回的isactive是true

固然如今在MetadataModule的參數還有其餘的寫法

Describe["GetProductByProductId"] = desc => desc.AsSwagger(
        with => with.Operation(
            op => op.OperationId("GetProductByProductId")
            .Tag("Products2")
            .Summary("Get a product by product's id")
            .Description("This returns a product's infomation by the special id")
            .Parameters(new List<Parameter>
            {
                new Parameter{Name = "productid",In = ParameterIn.Path,Required = true,Description = "id of a product"},
                new Parameter{Name = "isactive",In = ParameterIn.Query,Description = "get the actived product",Required = false}
            })
            .ProduceMimeType("application/json")
            .Response(r => r.Schema<Product>(modelCatalog).Description("Here is the product"))
            .Response(404, r => r.Description("Can't find the product"))
            ));

能夠用Parameters直接將全部的參數,組合成一個集合來進行處理。

此時的效果和上面是同樣的。

請求頭參數和請求體參數

在Module中添加一個新增商品的方法,這個方法包含兩種請求參數,一種是正常POST的json格式的數據,一種是請求頭,對於請求頭,只是判斷了一下客戶端發起的請求有沒有包含相應的請求頭就是了,並無作嚴格的判斷。同時爲了演示多種MIME類型的返回結果,這裏兼容了json和xml格式的返回結果。

Post("/", _ =>
{
    var product = this.Bind<Product>();

    if(!Request.Headers.Any(x=>x.Key=="test"))
    {
        return HttpStatusCode.BadRequest;
    }

    return Negotiate
        .WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/json"), product)
        .WithMediaRangeModel(new Nancy.Responses.Negotiation.MediaRange("application/xml"), product)
        ;
}, null, "AddProduct");

一樣的,MetadataModule中添加以下的描述:

Describe["AddProduct"] = desc => desc.AsSwagger(
with => with.Operation(
    op => op.OperationId("AddProduct")
            .Tag("Products")
            .Summary("Add a new product to database")
            .Description("This returns the added product's infomation")
            .BodyParameter(para=>para.Name("para").Schema<Product>().Description("the infomation of the adding product").Build())//Request body
            .Parameter(new Parameter()
            {
                Name = "test",
                In = ParameterIn.Header,//http請求頭
                Description = "must be not null",
                Required = true,
            })           
            .ConsumeMimeType("application/json") //post的參數只容許是json格式            
            .ProduceMimeTypes(new List<string>{ "application/json","application/xml" })//結果支持json和xml
            .Response(r => r.Schema<Product>(modelCatalog).Description("Here is the added product"))
            .Response(400, r => r.Description("Some errors occur during the processing"))
));

BodyParameter是咱們在POST等操做時用的,它須要指定咱們POST的數據格式(Schema那裏的類型),爲了演示添加請求頭信息,因此這裏也加了一個必填的請求頭信息。

ConsumeMimeType表示咱們發起請求的數據格式必須是json格式的,固然也能夠支持多種不一樣的數據格式。

ProduceMimeTypes表示服務端響應時支持的數據格式,這裏指定了json和Xml也是爲了和咱們Module中的內容相對應。

演示效果:

標註過期API和一個API屬於多個分組

有時候,API的界限分的不是很清晰或者有交集的時候,可能會出現這樣的狀況:一個api會屬於多個分組。

前面咱們都是直接指定了一個tag,也就表示上面的只是對應一個tag。

先來定義一個方法,用於演示多分組和過期、廢棄的API

Head("/",_=>
{
    return HttpStatusCode.OK;
},null,"HeadOfProduct");

Metadata內容

Describe["HeadOfProduct"] = desc => desc.AsSwagger(
    with => with.Operation(
        op => op.OperationId("HeadOfProduct")
                .Tags(new List<string>() { "Products", "Products2" })//同時屬於兩個分組
                .Summary("Something is deprecated")
                .Description("This returns only http header")
                .IsDeprecated()//過期的,至關於經常使用的Obsolete,可是還能夠用
                .Response(r => r.Description("Nothing will return but http headers"))                
    ));

效果以下:

雖然說已經標記爲過期了,可是本質這個方法仍是存在,因此也是能正常調用的。

安全認證問題

Swagger支持3種安全認證折方式:APIKEY、Basic、OAuth2.0,一樣的Nancy.Swagger也支持,不過有點坑就是了。

使用的話有兩個步驟(這裏用最簡單的APIKEY演示):

Step 1: 引用定義,在Bootstrapper中添加驗證相關的內容

protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)
{
    SwaggerMetadataProvider.SetInfo("Nancy Swagger Example", "v1.0", "Some open api", new Contact()
    {
        EmailAddress = "catcher_hwq@outlook.com",
        Name = "Catcher Wong",
        Url = "http://www.cnblogs.com/catcher1994",
    }, "http://www.cnblogs.com/catcher1994");
    
    var securitySchemeBuilder = new ApiKeySecuritySchemeBuilder();
    securitySchemeBuilder.Description("Authentication with apikey");
    securitySchemeBuilder.IsInQuery();
    securitySchemeBuilder.Name("Item1");           
    SwaggerMetadataProvider.AddSecuritySchemeBuilder(securitySchemeBuilder, "Item1");

    base.ApplicationStartup(container, pipelines);
}

Step 2 : 在MetadataModule中添加描述

Describe["Head"] = description => description.AsSwagger(
    with => with.Operation(
        op => op.OperationId("Head")
            .Tag("Head method")    
            .SecurityRequirement(SecuritySchemes.ApiKey)
            .Summary("an example head method")
            .Response(r => r.Description("OK"))));

固然,目前是沒有辦法正常運行的!此時運行效果以下:

單獨打開/api-docs這個路徑時提示以下錯誤:

這個十有八九是Nancy.Swagger的安全驗證存在bug的,這個項目沒有足夠多的單元測試可能也是致使問題的一部分緣由。

發現的主要bug是在MetadataModule中使用SecurityRequirement(SecuritySchemes.ApiKey)時一直在報錯,報錯內容以下:

Nancy.RequestExecutionException: Oh noes! ---< System.InvalidCastException: Unable to cast object of type 'Swagger.ObjectModel.SecuritySchemes' to type 'System.String'.
at Swagger.ObjectModel.SwaggerModel.SwaggerSerializerStrategy.ToObject(IDictionary source)

因而調試源碼,發如今Swagger.ObjectModel項目下的ToObject方法有問題

private static dynamic ToObject(IDictionary source)
{
    var expando = new ExpandoObject();
    var expandoCollection = (ICollection<KeyValuePair<string, object>>)expando;

    foreach (string key in source.Keys)
    {
        expandoCollection.Add(new KeyValuePair<string, object>(key, source[key]));
    }

    return expando;
}

從上面的出錯內容也能清楚的看到,SecuritySchemes不能轉成string的,其中SecuritySchemes是一個枚舉類型。

爲了能正常運行,確定要修改驗證一下!!因而修改爲以下 :

private static dynamic ToObject(IDictionary source)
{
    var expando = new ExpandoObject();
    var expandoCollection = (ICollection<KeyValuePair<string, object>>)expando;
    //用了var,在使用的時候強制ToString一下將其轉成string
    foreach (var key in source.Keys)
    {
        expandoCollection.Add(new KeyValuePair<string, object>(key.ToString(), source[key]));
    }

    return expando;
}

因爲在Mac上沒法打開這個項目,因此上面的修改是切換回windows完成的。

進行上面的修改後,項目是已經能正常運行了!可是卻少了一個很重要的東西!

在這個方法裏面加了APIKEY驗證的,可是小鎖的標記卻沒有出來!

以後對比了Swagger的官方示例http://petstore.swagger.io/

竟然有這麼坑爹的事情!security是一個數組啊,不是一個對象啊~~

後面就修改了Nancy.Swagger裏面的許多代碼(瞎改的,只爲了能正常運行),涉及了好幾個類文件,就不一一說明了。

第一個問題已經提了PR到這個項目了,第二個問題還沒找到比較滿意的方案,暫時沒提。

直接上最後的效果圖,分別演示了,沒有驗證,驗證成功和驗證失敗這三種狀況!

注:本文只演示了其中Nancy.Swagger的其中一種用法,並且還有部份內容是沒有涉及到的。還有兩種其餘用法有時間會拿出來和你們分享。

注意事項

在過程當中還有一個須要十分注意的地方(原本這個應該是在上一篇說起的):就是XXModule和XXMetadataModule相對應的位置關係。

Nancy在這裏限制的比較死,強制了下面三種狀況:

Module所在的位置 MetadtaModule應該在的位置
./BlahModule ./BlahMetadataModule
./BlahModule ./Metadata/BlahMetadataModule
./Modules/BlahModule ../Metadata/BlahMetadataModule

這是文件分佈所要注意的問題。

還有一個命名應該注意的問題:當咱們對一個Module起名爲ProductsModule時,它對應的MetadataModule必定要是ProductsMetadataModule。

而不能是其它,有一次因爲粗心,忘記把s字母帶上,花了很多時間去找緣由~~

上述兩個問題的答案在Nancy.Metadata.Modules項目的DefaultMetadataModuleConventions類中。

簡單總結

Nancy.Swagger給咱們API文檔化的道路上帶來了很多的便利之處,除了安全驗證這一塊的問題有點坑,其餘的算是比較正常,用起來也還算簡單。

對於Swagger來講,通用性很好,只要提供的指定格式的數據就能很好的渲染出讓人溫馨的界面,或許這就是它這麼流行的一個關鍵點吧。

下面是一張腦圖簡單的歸納相關的內容 :

本文已同步到Catcher寫的Nancy彙總博客:Nancy之大雜燴

相關文章
相關標籤/搜索