天天看点

.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的技术小白