天天看點

.net core的Swagger接口文檔使用教程(二):NSwag

  上一篇介紹了Swashbuckle ,位址:.net core的Swagger接口文檔使用教程(一):Swashbuckle

  講的東西還挺多,怎奈微軟還推薦了一個NSwag,那就繼續寫吧!

  但是和Swashbuckle一樣,如果還是按照那樣寫,東西有點多了,是以這裡就偷個懶吧,和Swashbuckle對照的去寫,介紹一些常用的東西算了,是以建議看完上一篇再繼續這裡。

  

  一、一般用法

  注:這裡一般用法的Demo源碼已上傳到百度雲:https://pan.baidu.com/s/1Z4Z9H9nto_CbNiAZIxpFFQ (提取碼:pa8s ),下面第二、三部分的功能可在Demo源碼基礎上去嘗試。

  建立一個.net core項目(這裡采用的是.net core3.1),然後使用nuget安裝NSwag.AspNetCore,建議安裝最新版本。

  同樣的,假如有一個接口:  

/// <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.AddOpenApiDocument(settings =>
        {
            settings.DocumentName = "v1";
            settings.Version = "v0.0.1";
            settings.Title = "測試接口項目";
            settings.Description = "接口文檔說明";
        });

        ...
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
      ...

        app.UseOpenApi();
        app.UseSwaggerUi3();

        ...
    }      

  然後運作項目,輸入http://localhost:5000/swagger,得到接口文檔頁面:

.net core的Swagger接口文檔使用教程(二):NSwag

   點選Try it out可以直接調用接口。

   同樣的,這裡的接口沒有注解,不太友好,可以和Swashbuckle一樣生成xml注釋檔案加載:

  右鍵項目=》切換到生成(Build),在最下面輸出輸出中勾選【XML文檔檔案】,同時,在錯誤警告的取消顯示警告中添加1591代碼:

.net core的Swagger接口文檔使用教程(二):NSwag

   不過,與Swashbuckle不一樣的是,Swashbuckle需要使用IncludeXmlComments方法加載注釋檔案,如果注釋檔案不存在,IncludeXmlComments方法還會抛出異常,但是NSwag不需要手動加載,預設xml注釋檔案和它對應點dll應該放在同一目錄且同名才能完成加載!

   按照上面的操作,運作項目後,接口就有注解了:

.net core的Swagger接口文檔使用教程(二):NSwag

   但是控制器标簽欄還是沒有注解,這是因為NSwag的控制器标簽預設從OpenApiTagAttribute中讀取   

[OpenApiTag("測試标簽",Description = "測試接口")]
    public class HomeController : ControllerBase      

  運作後顯示:

.net core的Swagger接口文檔使用教程(二):NSwag

    其實還可以修改這個預設行為,settings有一個UseControllerSummaryAsTagDescription屬性,将它設定成 true就可以從xml注釋檔案中加載描述了:  

services.AddOpenApiDocument(settings =>
    {
        ...

        //可以設定從注釋檔案加載,但是加載的内容可被OpenApiTagAttribute特性覆寫
        settings.UseControllerSummaryAsTagDescription = true;
    });      
.net core的Swagger接口文檔使用教程(二):NSwag

    接着是認證,比如JwtBearer認證,這個和Swashbuckle是類似的,隻不過拓展方法換成了AddSecurity:  

public void ConfigureServices(IServiceCollection services)
    {
        services.AddOpenApiDocument(settings =>
        {
            settings.DocumentName = "v1";
            settings.Version = "v0.0.1";
            settings.Title = "測試接口項目";
            settings.Description = "接口文檔說明";

            //可以設定從注釋檔案加載,但是加載的内容可悲OpenApiTagAttribute特性覆寫
            settings.UseControllerSummaryAsTagDescription = true;

            //定義JwtBearer認證方式一
            settings.AddSecurity("JwtBearer", Enumerable.Empty<string>(), new OpenApiSecurityScheme()
            {
                Description = "這是方式一(直接在輸入框中輸入認證資訊,不需要在開頭添加Bearer)",
                Name = "Authorization",//jwt預設的參數名稱
                In = OpenApiSecurityApiKeyLocation.Header,//jwt預設存放Authorization資訊的位置(請求頭中)
                Type = OpenApiSecuritySchemeType.Http,
                Scheme = "bearer"
            });

            //定義JwtBearer認證方式二
            settings.AddSecurity("JwtBearer", Enumerable.Empty<string>(), new OpenApiSecurityScheme()
            {
                Description = "這是方式二(JWT授權(資料将在請求頭中進行傳輸) 直接在下框中輸入Bearer {token}(注意兩者之間是一個空格))",
                Name = "Authorization",//jwt預設的參數名稱
                In = OpenApiSecurityApiKeyLocation.Header,//jwt預設存放Authorization資訊的位置(請求頭中)
                Type = OpenApiSecuritySchemeType.ApiKey
            });
        });

        ...
    }      

  到這裡,就是NSwag的一般用法了,可以滿足一般的需求了。

  二、服務注入(AddOpenApiDocument和AddSwaggerDocument)

  NSwag注入服務有兩個方法:AddOpenApiDocument和AddSwaggerDocument,兩者的差別就是架構類型不一樣,AddOpenApiDocument的SchemaType使用的是OpenApi3,AddSwaggerDocument的SchemaType使用的是Swagger2:  

/// <summary>Adds services required for Swagger 2.0 generation (change document settings to generate OpenAPI 3.0).</summary>
    /// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
    /// <param name="configure">Configure the document.</param>
    public static IServiceCollection AddOpenApiDocument(this IServiceCollection serviceCollection, Action<AspNetCoreOpenApiDocumentGeneratorSettings, IServiceProvider> configure = null)
    {
        return AddSwaggerDocument(serviceCollection, (settings, services) =>
        {
            settings.SchemaType = SchemaType.OpenApi3;
            configure?.Invoke(settings, services);
        });
    }
    /// <summary>Adds services required for Swagger 2.0 generation (change document settings to generate OpenAPI 3.0).</summary>
    /// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
    /// <param name="configure">Configure the document.</param>
    public static IServiceCollection AddSwaggerDocument(this IServiceCollection serviceCollection, Action<AspNetCoreOpenApiDocumentGeneratorSettings, IServiceProvider> configure = null)
    {
        serviceCollection.AddSingleton(services =>
        {
            var settings = new AspNetCoreOpenApiDocumentGeneratorSettings
            {
                SchemaType = SchemaType.Swagger2,
            };

            configure?.Invoke(settings, services);

            ...
        });

        ...
    }      

  個人推薦使用AddOpenApiDocument。  

services.AddOpenApiDocument(settings =>
    {
        //添加代碼
    });      

  同樣的,無論是AddOpenApiDocument還是AddSwaggerDocument,最終都是依賴AspNetCoreOpenApiDocumentGeneratorSettings來完成,與Swashbuckle不同的是,AddOpenApiDocument方法每次調用隻會生成一個swagger接口文檔對象,從上面的例子也能看出來:

  DocumentName

  接口文檔名,也就是Swashbuckle中SwaggerDoc方法中的name參數。

  Version

  接口文檔版本,也就是Swashbuckle中SwaggerDoc方法中的第二個OpenApiInfo的Version屬性。

  Title

  接口項目名稱,也就是Swashbuckle中SwaggerDoc方法中的第二個OpenApiInfo的Title屬性。

  Description

  接口項目介紹,也就是Swashbuckle中SwaggerDoc方法中的第二個OpenApiInfo的Description屬性。

  PostProcess

  這個是一個委托,在生成SwaggerDocument之後執行,需要注意的是,因為NSwag有緩存機制的存在PostProcess可能隻會執行一遍。

  比如:因為NSwag沒有直接提供Swashbuckle中SwaggerDoc方法中的第二個OpenApiInfo的Contact屬性的配置,這時我們可以使用PostProcess實作。  

settings.PostProcess = document =>
    {
        document.Info.Contact = new OpenApiContact()
        {
            Name = "zhangsan",
            Email = "[email protected]",
            Url = null
        };
    };      

   ApiGroupNames

   無論是Swashbuckle還是NSwag都支援生成多個接口文檔,但是在接口與文檔歸屬上不一緻:

  在Swashbuckle中,通過ApiExplorerSettingsAttribute特性的GroupName屬性指定documentName來實作的,而NSwag雖然也是用ApiExplorerSettingsAttribute特性實作,但是此時的GroupName不在是documentName,而是ApiGroupNames屬性指定的元素值了:

  比如下面三個接口:  

/// <summary>
    /// 未使用ApiExplorerSettings特性,表名屬于每一個swagger文檔
    /// </summary>
    /// <returns>結果</returns>
    [HttpGet("All"), Authorize]
    public string All()
    {
        return "All";
    }
    /// <summary>
    /// 使用ApiExplorerSettings特性表名該接口屬于swagger文檔v1
    /// </summary>
    /// <returns>Get結果</returns>
    [HttpGet]
    [ApiExplorerSettings(GroupName = "demo1")]
    public string Get()
    {
        return "Get";
    }
    /// <summary>
    /// 使用ApiExplorerSettings特性表名該接口屬于swagger文檔v2
    /// </summary>
    /// <returns>Post結果</returns>
    [HttpPost]
    [ApiExplorerSettings(GroupName = "demo2")]
    public string Post()
    {
        return "Post";
    }      

   定義兩個文檔:  

services.AddOpenApiDocument(settings =>
    {
        settings.DocumentName = "v1";
        settings.Version = "v0.0.1";
        settings.Title = "測試接口項目";
        settings.Description = "接口文檔說明";
        settings.ApiGroupNames = new string[] { "demo1" };

        settings.PostProcess = document =>
        {
            document.Info.Contact = new OpenApiContact()
            {
                Name = "zhangsan",
                Email = "[email protected]",
                Url = null
            };
        };
    });
    services.AddOpenApiDocument(settings =>
    {
        settings.DocumentName = "v2";
        settings.Version = "v0.0.2";
        settings.Title = "測試接口項目v0.0.2";
        settings.Description = "接口文檔說明v0.0.2";
        settings.ApiGroupNames = new string[] { "demo2" };

        settings.PostProcess = document =>
        {
            document.Info.Contact = new OpenApiContact()
            {
                Name = "lisi",
                Email = "[email protected]",
                Url = null
            };
        };
    });      

  這時不用像Swashbuckle還要在中間件中添加文檔位址,NSwag中間件會自動根據路由模闆和文檔生成文檔位址資訊,是以直接運作就可以了:

.net core的Swagger接口文檔使用教程(二):NSwag

   

.net core的Swagger接口文檔使用教程(二):NSwag

  可以注意到,All既不屬于v1文檔也不屬于v2文檔,也就是說,如果設定了ApiGroupNames,那就回嚴格的按ApiGroupNames來比較,隻有比對的GroupName在ApiGroupNames屬性中才算屬于這個接口文檔,這也是NSwag和Swashbuckle不同的一點。

  另外,同樣的,NSwag也支援使用IActionModelConvention和IControllerModelConvention設定GroupName,具體可以參考上一篇博文。

  UseControllerSummaryAsTagDescription   

  這個屬性上面例子有介紹,因為NSwag的控制器标簽預設從OpenApiTagAttribute中讀取,而不是從注釋文檔讀取,将此屬性設定成 true就可以從注釋文檔讀取了,但是讀取的内容可被OpenApiTagAttribute特性覆寫。

  AddSecurity

  AddSecurity拓展方法用于添加認證,它是兩個重載方法:  

public static OpenApiDocumentGeneratorSettings AddSecurity(this OpenApiDocumentGeneratorSettings settings, string name, OpenApiSecurityScheme swaggerSecurityScheme);
    public static OpenApiDocumentGeneratorSettings AddSecurity(this OpenApiDocumentGeneratorSettings settings, string name, IEnumerable<string> globalScopeNames, OpenApiSecurityScheme swaggerSecurityScheme);      

  雖然是重載,但是兩個方法的作用差别還挺大,第一個(不帶globalScopeNames參數)的方法的作用類似Swashbuckle的AddSecurityDefinition方法,隻是聲明的作用,而第二個(有globalScopeNames參數)的方法作用類似于Swashbuckle的AddSecurityRequirement方法,也就是說,這兩個重載方法,一個僅僅是聲明認證,另一個是除了聲明認證,還會将認證全局的作用于每個接口,不過這兩個方法的實作是使用DocumentProcessors(類似Swashbuckle的DocumentFilter)來實作的  

/// <summary>Appends the OAuth2 security scheme and requirement to the document's security definitions.</summary>
    /// <remarks>Adds a <see cref="SecurityDefinitionAppender"/> document processor with the given arguments.</remarks>
    /// <param name="settings">The settings.</param>
    /// <param name="name">The name/key of the security scheme/definition.</param>
    /// <param name="swaggerSecurityScheme">The Swagger security scheme.</param>
    public static OpenApiDocumentGeneratorSettings AddSecurity(this OpenApiDocumentGeneratorSettings settings, string name, OpenApiSecurityScheme swaggerSecurityScheme)
    {
        settings.DocumentProcessors.Add(new SecurityDefinitionAppender(name, swaggerSecurityScheme));
        return settings;
    }

    /// <summary>Appends the OAuth2 security scheme and requirement to the document's security definitions.</summary>
    /// <remarks>Adds a <see cref="SecurityDefinitionAppender"/> document processor with the given arguments.</remarks>
    /// <param name="settings">The settings.</param>
    /// <param name="name">The name/key of the security scheme/definition.</param>
    /// <param name="globalScopeNames">The global scope names to add to as security requirement with the scheme name in the document's 'security' property (can be an empty list).</param>
    /// <param name="swaggerSecurityScheme">The Swagger security scheme.</param>
    public static OpenApiDocumentGeneratorSettings AddSecurity(this OpenApiDocumentGeneratorSettings settings, string name, IEnumerable<string> globalScopeNames, OpenApiSecurityScheme swaggerSecurityScheme)
    {
        settings.DocumentProcessors.Add(new SecurityDefinitionAppender(name, globalScopeNames, swaggerSecurityScheme));
        return settings;
    }      

   而SecurityDefinitionAppender是一個實作了IDocumentProcessor接口的類,它實作的Porcess如下,其中_scopeNames就是上面方法傳進來的globalScopeNames:

/// <summary>Processes the specified Swagger document.</summary>
    /// <param name="context"></param>
    public void Process(DocumentProcessorContext context)
    {
        context.Document.SecurityDefinitions[_name] = _swaggerSecurityScheme;

        if (_scopeNames != null)
        {
            if (context.Document.Security == null)
            {
                context.Document.Security = new Collection<OpenApiSecurityRequirement>();
            }

            context.Document.Security.Add(new OpenApiSecurityRequirement
            {
                { _name, _scopeNames }
            });
        }
    }      

   至于其他用法,可以參考上面的一般用法和上一篇中介紹的Swashbuckle的AddSecurityDefinition方法和AddSecurityRequirement方法的用法,很相似。

  DocumentProcessors

  DocumentProcessors類似于Swashbuckle的DocumentFilter方法,隻不過DocumentFilter方法時實作IDocumentFilter接口,而DocumentProcessors一個IDocumentProcessor集合屬性,是需要實作IDocumentProcessor接口然後添加到集合中去。需要注意的是,因為NSwag有緩存機制的存在DocumentProcessors可能隻會執行一遍。

  另外,你可能注意到,上面有介紹過一個PostProcess方法,其實個人覺得PostProcess和DocumentProcessors差別不大,但是DocumentProcessors是在PostProcess之前調用執行,源碼中:  

public async Task<OpenApiDocument> GenerateAsync(ApiDescriptionGroupCollection apiDescriptionGroups)
    {
        ...

      foreach (var processor in Settings.DocumentProcessors)
        {
            processor.Process(new DocumentProcessorContext(document, controllerTypes, usedControllerTypes, schemaResolver, Settings.SchemaGenerator, Settings));
        }

        Settings.PostProcess?.Invoke(document);
        return document;
    }      

   可能是作者覺得DocumentProcessors有點繞,是以提供了一個委托供我們簡單處理吧,用法也可以參考上一篇中的Swashbuckle的DocumentFilter方法,比如全局的添加認證,全局的添加Server等等。

  OperationProcessors

   OperationProcessors類似Swashbuckle的OperationFilter方法,隻不過OperationFilter實作的是IOperationFilter,而OperationProcessors是IOperationProcessor接口集合。需要注意的是,因為NSwag有緩存機制的存在OperationProcessors可能隻會執行一遍。

  同樣的,可能作者為了友善我們使用,已經定義好了一個OperationProcessor類,我們可以将我們的邏輯當做參數去執行個體化OperationProcessor類,然後添加到OperationProcessors集合中即可,不過作者還提供了一個AddOperationFilter方法,可以往OperationProcessors即可開始位置添加過期操作:  

/// <summary>Inserts a function based operation processor at the beginning of the pipeline to be used to filter operations.</summary>
    /// <param name="filter">The processor filter.</param>
    public void AddOperationFilter(Func<OperationProcessorContext, bool> filter)
    {
        OperationProcessors.Insert(0, new OperationProcessor(filter));
    }      

   是以我們可以這麼用:  

settings.AddOperationFilter(context =>
    {
        //我們的邏輯
        return true;
    });      

  另外,因為無論使用AddOperationFilter方法,還是直接往OperationProcessors集合中添加IOperationProcessor對象,都會對所有Action(或者說Operation)進行調用,NSwag還有一個SwaggerOperationProcessorAttribute特性(新版已改為OpenApiOperationProcessorAttribute),用于指定某些特定Action才會調用執行。當然,SwaggerOperationProcessorAttribute的執行個體化需要指定一個實作了IOperationProcessor接口的類型以及執行個體化它所需要的的參數。

  與Swashbuckle不同的是,IOperationProcessor的Process接口要求傳回一個bool類型的值,表示接口是否要在swaggerUI頁面展示,如果傳回false,接口就不會在前端展示了,而且後續的IOperationProcessor對象也不再繼續調用執行。  

private bool RunOperationProcessors(OpenApiDocument document, Type controllerType, MethodInfo methodInfo, OpenApiOperationDescription operationDescription, List<OpenApiOperationDescription> allOperations, OpenApiDocumentGenerator swaggerGenerator, OpenApiSchemaResolver schemaResolver)
    {
        var context = new OperationProcessorContext(document, operationDescription, controllerType,
            methodInfo, swaggerGenerator, Settings.SchemaGenerator, schemaResolver, Settings, allOperations);

        // 1. Run from settings
        foreach (var operationProcessor in Settings.OperationProcessors)
        {
            if (operationProcessor.Process(context)== false)
            {
                return false;
            }
        }

        // 2. Run from class attributes
        var operationProcessorAttribute = methodInfo.DeclaringType.GetTypeInfo()
            .GetCustomAttributes()
        // 3. Run from method attributes
            .Concat(methodInfo.GetCustomAttributes())
            .Where(a => a.GetType().IsAssignableToTypeName("SwaggerOperationProcessorAttribute", TypeNameStyle.Name));

        foreach (dynamic attribute in operationProcessorAttribute)
        {
            var operationProcessor = ObjectExtensions.HasProperty(attribute, "Parameters") ?
                (IOperationProcessor)Activator.CreateInstance(attribute.Type, attribute.Parameters) :
                (IOperationProcessor)Activator.CreateInstance(attribute.Type);

            if (operationProcessor.Process(context) == false)
            {
                return false;
            }
        }

        return true;
    }      

   至于其它具體用法,具體用法可以參考上一篇介紹的Swashbuckle的OperationFilter方法,如給特定Operation添加認證,或者對響應接口包裝等等。

  SchemaProcessors

  SchemaFilter的作用類似Swashbuckle的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();
    }      

  直接運作後得到的傳回類型的說明是這樣的:

.net core的Swagger接口文檔使用教程(二):NSwag

  這就有個問題了,枚舉類型中的0、1、2等等就是何含義,這個沒有在swagger中展現出來,這個時候我們可以通過SchemaProcessors來修改Schema資訊。

  比如,可以先用一個特性(例如使用DescriptionAttribute)辨別枚舉類型的每一項,用于說明含義:  

public enum SexEnum
    {
        /// <summary>
        /// 未知
        /// </summary>
        [Description("未知")]
        Unknown = 0,
        /// <summary>
        /// 男
        /// </summary>
        [Description("男")]
        Male = 1,
        /// <summary>
        /// 女
        /// </summary>
        [Description("女")]
        Female = 2
    }      

  接着我們建立一個MySchemaProcessor類,實作ISchemaProcessor接口:  

.net core的Swagger接口文檔使用教程(二):NSwag
.net core的Swagger接口文檔使用教程(二):NSwag
public class MySchemaProcessor : ISchemaProcessor
    {
        static readonly ConcurrentDictionary<Type, Tuple<string, object>[]> dict = new ConcurrentDictionary<Type, Tuple<string, object>[]>();
        public void Process(SchemaProcessorContext context)
        {
            var schema = context.Schema;
            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);
            }
        }
        private void UpdateSchemaDescription(JsonSchema schema)
        {
            if (schema.HasReference)
            {
                var s = schema.ActualSchema;
                if (s != null && s.Enumeration != null && s.Enumeration.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);
            }
        }
        /// <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;
        }
    }      

MySchemaProcessor

  最後在Startup中使用這個MySchemaProcessor類:  

services.AddOpenApiDocument(settings =>
    {
        ...

        settings.SchemaProcessors.Add(new MySchemaProcessor());
    });      

  再次運作項目後,得到的架構就有每個枚舉項的屬性了,當然,你也可以安裝自己的意願去生成特定格式的架構,這隻是一個簡單的例子

.net core的Swagger接口文檔使用教程(二):NSwag

  其它配置

  AspNetCoreOpenApiDocumentGeneratorSettings繼承于OpenApiDocumentGeneratorSettings和JsonSchemaGeneratorSettings還有茫茫多的配置,感興趣的自己看源碼吧,畢竟它和Swashbuckle差不多,一般的需求都能滿足了,實作滿足不了,可以使用DocumentProcessors和OperationProcessors來實作,就跟Swashbuckle的DocumentFilter和OperationFilter一樣。

  但是有些問題可能就不行了,比如虛拟路徑問題,Swashbuckle采用在Server上加路徑來實作,而因為NSwag沒有像Swashbuckle的AddServer方法,想到可以使用上面的PostProcess方法或者使用DocumentProcessors來實作,但是現實是打臉,因為作者的處理方式是,執行PostProcess方法和DocumentProcessors之後,會把OpenAPIDocument上的Servers先清空,然後再加上目前SwaggerUI所在的域名位址,可能作者覺着這樣能滿足大部分人的需求吧。但是作者還是提供了其他的方式來操作,會在後面的中間件中介紹

  三、添加Swagger中間件(UseOpenApi、UseSwagger和UseSwaggerUi3、UseSwaggerUi)

  UseOpenApi、UseSwagger

  首先UseOpenApi、UseSwagger和Swashbuckle的UseSwagger的作用一樣的,主要用于攔截swagger.json請求,進而可以擷取傳回所需的接口架構資訊,不同點在于NSwag的UseOpenApi、UseSwagger具有緩存機制,也就是說,如果第一次擷取到了接口文檔,會已json格式将文檔加入到本地緩存中,下次直接從緩存擷取,因為緩存的存在,是以上面介紹的OperationProcessors和DocumentProcessors都不會再執行了。

  另外,UseSwagger是舊版本,已經不推薦使用了,推薦使用UseOpenApi:  

app.UseOpenApi(settings =>
    {
        //中間件設定
    });      

  OpenApiDocumentMiddlewareSettings

  UseOpenApi依賴OpenApiDocumentMiddlewareSettings對象完成配置過程,主要屬性有:

  Path

  Path表示攔截請求的格式,也就是攔截swagger.json的路由格式,這個跟Swashbuckle一樣,因為需要從路由知道是哪個文檔,然後才能去找這個文檔的所有接口解析傳回,它的預設值是 /swagger/{documentName}/swagger.json。

  同樣的,因為這個值關系比較重要,盡可能不要去修改吧。

  從上面的Path參數的預設值中可以看到,其中有個{documentName}參數,NSwag并沒有要求Path中必須有{documentName}參數。

  如果沒有這個參數,就必須指定這個屬性DocumentName,隻是也就是說NSwag隻為一個接口文檔服務。

  如果有這個參數,NSwag會周遊所有定義的接口文檔,然後分别對Path屬性替換掉其中中的{documentName}參數,然後分别攔截每個文檔擷取架構資訊的swagger.json請求。

  服務注入部分有一個PostProcess方法,功能其實類似于DocumentProcessors,就是對接口文檔做一個調整,而現在這裡又有一個PostProcess方法,它則是根據目前請求來調整接口文檔用的。

  比如,上面有介紹,如果在服務注入部分使用PostProcess方法或者DocumentProcessors添加了Server,是沒有效果的,這個是因為NSwag在擷取到文檔之後,有意的清理了文檔的Servers屬性,然後加上了目前請求的位址:  

/// <summary>Generates the Swagger specification.</summary>
    /// <param name="context">The context.</param>
    /// <returns>The Swagger specification.</returns>
    protected virtual async Task<OpenApiDocument> GenerateDocumentAsync(HttpContext context)
    {
        var document = await _documentProvider.GenerateAsync(_documentName);

        document.Servers.Clear();
        document.Servers.Add(new OpenApiServer
        {
            Url = context.Request.GetServerUrl()
        });

        _settings.PostProcess?.Invoke(document, context.Request);

        return document;
    }      

  注意到上面的源碼,在清理之後,還調用了這個PostProcess委托,是以,我們可以将添加Server部分的代碼寫到這個PostProcess中:  

app.UseOpenApi(settings =>
    {
        settings.PostProcess = (document, request) =>
        {
            //清理掉NSwag加上去的
            document.Servers.Clear();
            document.Servers.Add(new OpenApiServer() { Url = "http://localhost:90/NSwag", Description = "位址1" });
            document.Servers.Add(new OpenApiServer() { Url = "http://127.0.0.1:90/NSwag", Description = "位址2" });
            //192.168.28.213是我本地IP
            document.Servers.Add(new OpenApiServer() { Url = "http://192.168.28.213:90/NSwag", Description = "位址3" });
        };
    });      

  看來,作者還是很友好的,做了點小動作還提供給我們一個修改的方法。

  CreateDocumentCacheKey

  上面有提到,NSwag的接口文旦有緩存機制,第一次擷取之後就會以json格式被緩存,接下就會從緩存中讀取,而CreateDocumentCacheKey就是緩存的鍵值工廠,用于生成緩存鍵值用的,如果不設定,那麼緩存的鍵值就是string.Empty。

  那可能會問,如果不想用緩存呢,不妨設定CreateDocumentCacheKey成這樣:  

app.UseOpenApi(settings =>
    {
        settings.CreateDocumentCacheKey = request => DateTime.Now.ToString();
    });      

  然後你就會發現,過了一段時間之後,你的程式挂了,OutOfMemory!

  是以,好好的用緩存的,從源碼中目前沒發現有什麼辦法可以取消緩存,況且使用緩存可以提高響應速度,為何不用?如果實在要屏蔽緩存,那就是改改源碼再編譯引用吧。

  ExceptionCacheTime

  既然是程式,那就有可能會抛出異常,擷取接口文檔架構也不例外,而ExceptionCacheTime表示在擷取接口文檔發生異常後的一段時間内,使用傳回這個異常,ExceptionCacheTime預設是TimeSpan.FromSeconds(10)

  UseSwaggerUi3、UseSwaggerUi

  UseSwaggerUi3、UseSwaggerUi的作用和Swashbuckle的UseSwaggerUI作用是一樣,主要用于攔截swagger/index.html頁面請求,傳回頁面給前端。

  UseSwaggerUi傳回的是基于Swagger2.0的頁面,而UseSwaggerUi3傳回的是基于Swagger3.0的頁面,是以這裡推薦使用UseSwaggerUi3  

app.UseSwaggerUi3(settings =>
    {
        //中間件操作
    });      

  SwaggerUi3Settings

  UseSwaggerUi3依賴SwaggerUi3Settings完成配置,SwaggerUi3Settings繼承于SwaggerUiSettingsBase和SwaggerSettings,是以屬性比較多,這裡介紹常用的一些屬性:

  EnableTryItOut

  這個屬性很簡單,就是設定允許你是否可以在SwaggerUI使用Try it out去調用接口

  DocumentTitle

  這是SwaggerUI頁面的Title資訊,也就是傳回的html的head标簽下的title标簽值,預設是 Swagger UI

  CustomHeadContent

  自定義頁面head标簽内容,可以使用自定義的腳本和樣式等等,作用于Swashbuckle中提到的HeadContent是一樣的

  Path是SwaggerUI的index.html頁面的位址,作用與Swashbuckle中提到的RoutePrefix是一樣的

  CustomInlineStyles

  自定外部樣式,不是連結,就是具體的樣式!

  自定義的外部樣式檔案的連結

  CustomJavaScriptPath

  自定義外部JavaScript腳本檔案的連接配接

  DocumentPath

  接口文檔擷取架構swagger.json的Url模闆,NSwag不需要想Swashbuckle調用SwaggerEndpoint添加文檔就是因為它會自動根據這個将所有文檔按照DocumentPath的格式進行設定,它的預設值是 /swagger/{documentName}/swagger.json。

  同樣的,盡可能不要修改這個屬性,如果修改了,切記要和上面介紹的OpenApiDocumentMiddlewareSettings的Path屬性同步修改。

  SwaggerRoutes

  這是屬性包含了接口文檔清單,在Swashbuckle中是通過SwaggerEndpoint方法添加的,但是NSwag會自動生成根據DocumentPath屬性自動生成。  

app.UseSwaggerUi3(settings =>
    {
        settings.SwaggerRoutes.Add(new NSwag.AspNetCore.SwaggerUi3Route("demo", "/swagger/v1/swagger.json"));
    });      

  需要注意的是,如果自己往SwaggerRoutes中添加接口文檔對象,那麼NSwag不會自動生成了,比如上面的例子,雖然定義了多個文檔,但是我們手動往SwaggerRoutes添加了一個,那SwaggerUI中就隻會顯示我們自己手動添加的了。

  TransformToExternalPath

  TransformToExternalPath其實是一個路徑轉化,主要是轉換swagger内部的連接配接,比如擷取架構新的的請求 /swagger/v1/swagger.json和擷取swaggerUI頁面的連接配接 /swagger,這個很有用,比如上面提到的虛拟路徑處理的一個完整的例子: 

.net core的Swagger接口文檔使用教程(二):NSwag
.net core的Swagger接口文檔使用教程(二):NSwag
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NSwag;

namespace NSwagDemo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddOpenApiDocument(settings =>
            {
                settings.DocumentName = "v1";
                settings.Version = "v0.0.1";
                settings.Title = "測試接口項目";
                settings.Description = "接口文檔說明";
                settings.ApiGroupNames = new string[] { "demo1" };

                settings.PostProcess = document =>
                {
                    document.Info.Contact = new OpenApiContact()
                    {
                        Name = "zhangsan",
                        Email = "[email protected]",
                        Url = null
                    };
                };

                settings.AddOperationFilter(context =>
                {
                    //我們的邏輯
                    return true;
                });

                //可以設定從注釋檔案加載,但是加載的内容可被OpenApiTagAttribute特性覆寫
                settings.UseControllerSummaryAsTagDescription = true;

                //定義JwtBearer認證方式一
                settings.AddSecurity("JwtBearer", Enumerable.Empty<string>(), new OpenApiSecurityScheme()
                {
                    Description = "這是方式一(直接在輸入框中輸入認證資訊,不需要在開頭添加Bearer)",
                    Name = "Authorization",//jwt預設的參數名稱
                    In = OpenApiSecurityApiKeyLocation.Header,//jwt預設存放Authorization資訊的位置(請求頭中)
                    Type = OpenApiSecuritySchemeType.Http,
                    Scheme = "bearer"
                });

                //定義JwtBearer認證方式二
                settings.AddSecurity("JwtBearer", Enumerable.Empty<string>(), new OpenApiSecurityScheme()
                {
                    Description = "這是方式二(JWT授權(資料将在請求頭中進行傳輸) 直接在下框中輸入Bearer {token}(注意兩者之間是一個空格))",
                    Name = "Authorization",//jwt預設的參數名稱
                    In = OpenApiSecurityApiKeyLocation.Header,//jwt預設存放Authorization資訊的位置(請求頭中)
                    Type = OpenApiSecuritySchemeType.ApiKey
                });
            });

            services.AddAuthentication();
            services.AddControllers();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseRouting();

            //NSwag是虛拟路徑
            var documentPath = "/swagger/{documentName}/swagger.json";
            app.UseOpenApi(settings =>
            {
                settings.PostProcess = (document, request) =>
                {
                    //清理掉NSwag加上去的
                    document.Servers.Clear();
                    document.Servers.Add(new OpenApiServer() { Url = "http://localhost:90/NSwag", Description = "位址1" });
                    document.Servers.Add(new OpenApiServer() { Url = "http://127.0.0.1:90/NSwag", Description = "位址2" });
                    //192.168.28.213是我本地IP
                    document.Servers.Add(new OpenApiServer() { Url = "http://192.168.28.213:90/NSwag", Description = "位址3" });
                };
                settings.Path = documentPath;
            });
            app.UseSwaggerUi3(settings =>
            {
                //settings.SwaggerRoutes.Add(new NSwag.AspNetCore.SwaggerUi3Route("demo", "/swagger/v1/swagger.json"));
                settings.TransformToExternalPath = (s, r) =>
                 {

                     if (s.EndsWith("swagger.json"))
                     {
                         return $"/NSwag{s}";
                     }
                     return s;
                 };
            });

            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapControllers();
            });
        }
    }
}      

虛拟路徑例子

  比如這裡我們的虛拟路徑是NSwag,使用IIS部署:

.net core的Swagger接口文檔使用教程(二):NSwag

    項目運作後

.net core的Swagger接口文檔使用教程(二):NSwag

  四、總結

   後面還有東西就不寫了,還是那三個注意點:

  主要就是記住三點:

  1、服務注入使用AddOpenApiDocument方法(盡量不要用AddSwaggerDocument),主要就是生成接口相關資訊,如認證,接口注釋等等,還有幾種過濾器幫助我們實作自己的需求

  2、中間件注入有兩個:UseOpenApi(盡量不要使用UseSwagger,後續版本将會被移除)和UseSwaggerUi3(盡量不要使用UseSwaggerUi,後續版本将會被移除):

     UseOpenApi負責傳回接口架構資訊,傳回的是json格式的資料

     UseSwaggerUi3負責傳回的是頁面資訊,傳回的是html内容

  3、如果涉及到接口生成的,盡可能在AddOpenApiDocument中實作,如果涉及到UI頁面的,盡可能在UseSwaggerUi3中實作

一個專注于.NetCore的技術小白