上一篇介绍了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 = "xxx@qq.com",
            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 = "xxx@qq.com",
                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 = "xxx@qq.com",
                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接口:  

  
    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

相关文章: