上一篇介绍了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,得到接口文档页面:
点击Try it out可以直接调用接口。
同样的,这里的接口没有注解,不太友好,可以和Swashbuckle一样生成xml注释文件加载:
右键项目=》切换到生成(Build),在最下面输出输出中勾选【XML文档文件】,同时,在错误警告的取消显示警告中添加1591代码:
不过,与Swashbuckle不一样的是,Swashbuckle需要使用IncludeXmlComments方法加载注释文件,如果注释文件不存在,IncludeXmlComments方法还会抛出异常,但是NSwag不需要手动加载,默认xml注释文件和它对应点dll应该放在同一目录且同名才能完成加载!
按照上面的操作,运行项目后,接口就有注解了:
但是控制器标签栏还是没有注解,这是因为NSwag的控制器标签默认从OpenApiTagAttribute中读取
[OpenApiTag("测试标签",Description = "测试接口")] public class HomeController : ControllerBase
运行后显示:
其实还可以修改这个默认行为,settings有一个UseControllerSummaryAsTagDescription属性,将它设置成 true就可以从xml注释文件中加载描述了:
services.AddOpenApiDocument(settings => { ... //可以设置从注释文件加载,但是加载的内容可被OpenApiTagAttribute特性覆盖 settings.UseControllerSummaryAsTagDescription = true; });
运行后显示:
接着是认证,比如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中间件会自动根据路由模板和文档生成文档地址信息,所以直接运行就可以了:
可以注意到,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(); }
直接运行后得到的返回类型的说明是这样的:
这就有个问题了,枚举类型中的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; } }