.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,得到接口文档页面:
点击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; } }
最后在Startup中使用这个MySchemaProcessor类:
services.AddOpenApiDocument(settings => { ... settings.SchemaProcessors.Add(new MySchemaProcessor()); });
再次运行项目后,得到的架构就有每个枚举项的属性了,当然,你也可以安装自己的意愿去生成特定格式的架构,这只是一个简单的例子
其它配置
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。
同样的,因为这个值关系比较重要,尽可能不要去修改吧。
DocumentName
从上面的Path参数的默认值中可以看到,其中有个{documentName}参数,NSwag并没有要求Path中必须有{documentName}参数。
如果没有这个参数,就必须指定这个属性DocumentName,只是也就是说NSwag只为一个接口文档服务。
如果有这个参数,NSwag会遍历所有定义的接口文档,然后分别对Path属性替换掉其中中的{documentName}参数,然后分别拦截每个文档获取架构信息的swagger.json请求。
PostProcess
服务注入部分有一个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
Path是SwaggerUI的index.html页面的地址,作用与Swashbuckle中提到的RoutePrefix是一样的
CustomInlineStyles
自定外部样式,不是链接,就是具体的样式!
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,这个很有用,比如上面提到的虚拟路径处理的一个完整的例子:
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 = "xxx@qq.com", 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部署:
项目运行后
四、总结
后面还有东西就不写了,还是那三个注意点:
主要就是记住三点:
1、服务注入使用AddOpenApiDocument方法(尽量不要用AddSwaggerDocument),主要就是生成接口相关信息,如认证,接口注释等等,还有几种过滤器帮助我们实现自己的需求
2、中间件注入有两个:UseOpenApi(尽量不要使用UseSwagger,后续版本将会被移除)和UseSwaggerUi3(尽量不要使用UseSwaggerUi,后续版本将会被移除):
UseOpenApi负责返回接口架构信息,返回的是json格式的数据
UseSwaggerUi3负责返回的是页面信息,返回的是html内容
3、如果涉及到接口生成的,尽可能在AddOpenApiDocument中实现,如果涉及到UI页面的,尽可能在UseSwaggerUi3中实现