.net core的Swagger接口文档使用教程(一):Swashbuckle
现在的开发大部分都是前后端分离的模式了,后端提供接口,前端调用接口。后端提供了接口,需要对接口进行测试,之前都是使用浏览器开发者工具,或者写单元测试,再或者直接使用Postman,但是现在这些都已经out了。后端提供了接口,如何跟前端配合说明接口的性质,参数,验证情况?这也是一个问题。有没有一种工具可以根据后端的接口自动生成接口文档,说明接口的性质,参数等信息,又能提供接口调用等相关功能呢?
答案是有的。Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务。而作为.net core开发,Swashbuckle是swagger应用的首选!本文旨在介绍Swashbuckle的一些常见功能,以满足大部分开发的需要!
本文旨在介绍Swashbuckle的一般用法以及一些常用方法,让读者读完之后对Swashbuckle的用法有个最基本的理解,可满足绝大部分需求的需要,比如认证问题、虚拟路劲问题,返回值格式问题等等。
如果对Swashbuckle源码感兴趣,可以去github上pull下来看看
github中Swashbuckle.AspNetCore源码地址:https://github.com/domaindrivendev/Swashbuckle.AspNetCore
一、一般用法
注:这里一般用法的Demo源码已上传到百度云:https://pan.baidu.com/s/1Z4Z9H9nto_CbNiAZIxpFFQ (提取码:pa8s ),下面第二、三部分的功能可在Demo源码基础上去尝试。
创建一个.net core项目(这里采用的是.net core3.1),然后使用nuget安装Swashbuckle.AspNetCore,建议安装5.0以上版本,因为swagger3.0开始已经加入到OpenApi项目中,因此Swashbuckle新旧版本用法还是有一些差异的。
比如,我们一个Home控制器:
/// <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.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo() { Version = "v0.0.1", Title = "swagger测试项目", Description = $"接口文档说明", Contact = new OpenApiContact() { Name = "zhangsan", Email = "xxx@qq.com", Url = null } }); }); ... }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { ... app.UseSwagger(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1"); });
... }
然后运行项目,输入http://localhost:5000/swagger,得到接口文档页面:
点击Try it out可以直接调用接口。
这里,发现接口没有注解说明,这不太友好,而Swashbuckle的接口可以从代码注释中获取,也可以使用代码说明,我们做开发的当然想直接从注释获取啦。
但是另一方面,因为注释在代码编译时会被过滤掉,因此我们需要在项目中生成注释文件,然后让程序加载注释文件,操作如下:
右键项目=》切换到生成(Build),在最下面输出输出中勾选【XML文档文件】,同时,在错误警告的取消显示警告中添加1591代码:
注:建议这里添加1591,因为如果不添加,而且勾选【XML文档文件】,那么如果代码中没有注释,项目将会抛出茫茫多的警告,而1591则表示取消这种无注释的警告
生成当前项目时会将项目中所有的注释打包到这个文件中。
然后修改ConfigureServices:
public void ConfigureServices(IServiceCollection services) {
...
services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo() { Version = "v0.0.1", Title = "swagger测试项目", Description = $"接口文档说明", Contact = new OpenApiContact() { Name = "zhangsan", Email = "xxx@qq.com", Url = null } }); options.IncludeXmlComments("SwashbuckleDemo.xml", true); }); ... }
上面使用IncludeXmlComments方法加载注释,第二个参数true表示注释文件包含了控制器的注释,如果不包含控制器注释(如引用的其他类库),可以将它置为false
注意上面的xml文件要与它对应的dll文件放到同目录,如果不在同一目录,需要自行指定目录,如果找不到文件,可能会抛出异常!。
另外,如果项目引用的其他项目,可以将其他项目也生成xml注释文件,然后使用IncludeXmlComments方法加载,从而避免部分接口信息无注解情况
运行后可以得到接口的注释:
接着,既然是提供接口,没有认证怎么行,比如,Home控制器下还有一个Post接口,但是接口需要认证,比如JwtBearer认证:
/// <summary> /// 测试接口 /// </summary> [ApiController] [Route("[controller]")] public class HomeController : ControllerBase { ... /// <summary> /// 使用认证获取数据 /// </summary> /// <returns>返回数据</returns> [HttpPost, Authorize] public string Post() { return "这是认证后的数据"; } }
为了接口能使用认证,修改Startup的ConfigureServices:
public void ConfigureServices(IServiceCollection services) {
...
services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo() { Version = "v0.0.1", Title = "swagger测试项目", Description = $"接口文档说明", Contact = new OpenApiContact() { Name = "zhangsan", Email = "xxx@qq.com", Url = null } }); options.IncludeXmlComments("SwashbuckleDemo.xml", true);//第二个参数true表示注释文件包含了控制器的注释 //定义JwtBearer认证方式一 options.AddSecurityDefinition("JwtBearer", new OpenApiSecurityScheme() { Description = "这是方式一(直接在输入框中输入认证信息,不需要在开头添加Bearer)", Name = "Authorization",//jwt默认的参数名称 In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中) Type = SecuritySchemeType.Http, Scheme = "bearer" }); //定义JwtBearer认证方式二 //options.AddSecurityDefinition("JwtBearer", new OpenApiSecurityScheme() //{ // Description = "这是方式二(JWT授权(数据将在请求头中进行传输) 直接在下框中输入Bearer {token}(注意两者之间是一个空格))", // Name = "Authorization",//jwt默认的参数名称 // In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中) // Type = SecuritySchemeType.ApiKey //}); //声明一个Scheme,注意下面的Id要和上面AddSecurityDefinition中的参数name一致 var scheme = new OpenApiSecurityScheme() { Reference = new OpenApiReference() { Type = ReferenceType.SecurityScheme, Id = "JwtBearer" } }; //注册全局认证(所有的接口都可以使用认证) options.AddSecurityRequirement(new OpenApiSecurityRequirement() { [scheme] = new string[0] }); });
... }
程序运行后效果如下:
上面说了,添加JwtBearer认证有两种方式,两种方式的区别如下:
到这里应该就已经满足大部分需求的用法了,这也是网上很容易就能搜索到的,接下来介绍的是一些常用到的方法。
二、服务注入(AddSwaggerGen)
前面介绍到,Swashbuckle的服务注入是在ConfigureServices中使用拓展方法AddSwaggerGen实现的
services.AddSwaggerGen(options => { //使用options注入服务 });
确切的说swagger的服务注入是使用SwaggerGenOptions来实现的,下面主要介绍SwaggerGenOptions的一些常用的方法:
SwaggerDoc
SwaggerDoc主要用来声明一个文档,上面的例子中声明了一个名称为v1的接口文档,当然,我们可以声明多个接口文档,比如按开发版本进行声明:
options.SwaggerDoc("v1", new OpenApiInfo() { Version = "v0.0.1", Title = "项目v0.0.1", Description = $"接口文档说明v0.0.1", Contact = new OpenApiContact() { Name = "zhangsan", Email = "xxx@qq.com", Url = null } }); options.SwaggerDoc("v2", new OpenApiInfo() { Version = "v0.0.2", Title = "项目v0.0.2", Description = $"接口文档说明v0.0.2", Contact = new OpenApiContact() { Name = "lisi", Email = "xxxx@qq.com", Url = null } });
...
开发过程中,可以将接口文档名称设置成枚举或者常量值,以方便文档名的使用。
至于上面OpenApiInfo声明的各参数,其实就是要在SwaggerUI页面上展示出来的,读者可自行测试一下,这里不过多说明,只是顺带提一下Description属性,这个是一个介绍文档接口的简介,但是这个属性是支持html展示的,也就是说可以生成一些html代码放到Description属性中。
声明多个文档,可以将接口进行归类,不然一个项目几百个接口,查看起来也不方便,而将要接口归属某个文档,我们可以使ApiExplorerSettingsAttribute指定GroupName来指定,如:
/// <summary> /// 未使用ApiExplorerSettings特性,表名属于每一个swagger文档 /// </summary> /// <returns>结果</returns> [HttpGet("All")] public string All() { return "All"; } /// <summary> /// 使用ApiExplorerSettings特性表名该接口属于swagger文档v1 /// </summary> /// <returns>Get结果</returns> [HttpGet] [ApiExplorerSettings(GroupName = "v1")] public string Get() { return "Get"; } /// <summary> /// 使用ApiExplorerSettings特性表名该接口属于swagger文档v2 /// </summary> /// <returns>Post结果</returns> [HttpPost] [ApiExplorerSettings(GroupName = "v2")] public string Post() { return "Post"; }
因为我们现在有两个接口文档了,想要在swaggerUI中看得到,还需要在中间件中添加相关文件的swagger.json文件的入口:
public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { ... app.UseSwagger(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "v1"); options.SwaggerEndpoint("/swagger/v2/swagger.json", "v2"); }); ... }
运行项目后:
上面使用ApiExplorerSettingsAttribute的GroupName属性指定归属的swagger文档(GroupName需要设置成上面SwaggerDoc声明的文档的名称),如果不使用ApiExplorerSettingsAttribute,那么接口将属于所有的swagger文档,上面的例子可以看到/Home/All接口既属于v1也属于v2。
另外ApiExplorerSettingsAttribute还有个IgnoreApi属性,如果设置成true,将不会在swagger页面展示该接口。
但是接口一个个的去添加ApiExplorerSettingsAttribute,是不是有点繁琐了?没事,我们可以采用Convertion实现,主要是IActionModelConvention和IControllerModelConvention两个:
IActionModelConvention方式:
public class GroupNameActionModelConvention : IActionModelConvention { public void Apply(ActionModel action) { if (action.Controller.ControllerName == "Home") { if (action.ActionName == "Get") { action.ApiExplorer.GroupName = "v1"; action.ApiExplorer.IsVisible = true; } else if (action.ActionName == "Post") { action.ApiExplorer.GroupName = "v2"; action.ApiExplorer.IsVisible = true; } } } }
然后在ConfigureService中使用:
services.AddControllers(options => { options.Conventions.Add(new GroupNameActionModelConvention()); });
或者使用IControllerModelConvention方式:
public class GroupNameControllerModelConvention : IControllerModelConvention { public void Apply(ControllerModel controller) { if (controller.ControllerName == "Home") { foreach (var action in controller.Actions) { if (action.ActionName == "Get") { action.ApiExplorer.GroupName = "v1"; action.ApiExplorer.IsVisible = true; } else if (action.ActionName == "Post") { action.ApiExplorer.GroupName = "v2"; action.ApiExplorer.IsVisible = true; } } } } }
然后在ConfigureService中使用:
services.AddControllers(options => { options.Conventions.Add(new GroupNameControllerModelConvention()); });
这两种方式实现的效果和使用ApiExplorerSettingsAttribute是一样的,细心的朋友可能会注意,action.ApiExplorer.GroupName与ApiExplorerSettingsAttribute.GroupName是对应的,action.ApiExplorer.IsVisible则与ApiExplorerSettingsAttribute.IgnoreApi是对应的
IncludeXmlComments
IncludeXmlComments是用于加载注释文件,Swashbuckle会从注释文件中去获取接口的注解,接口参数说明以及接口返回的参数说明等信息,这个在上面的一般用法中已经介绍了,这里不再重复说明
IgnoreObsoleteActions
IgnoreObsoleteActions表示过滤掉ObsoleteAttribute属性声明的接口,也就是说不会在SwaggerUI中显示接口了,ObsoleteAttribute修饰的接口表示接口已过期,尽可能不要再使用。
方法调用等价于:
options.SwaggerGeneratorOptions.IgnoreObsoleteActions = true;
IgnoreObsoleteProperties
IgnoreObsoleteProperties的作用类似于IgnoreObsoleteActions,只不过IgnoreObsoleteActions是作用于接口,而IgnoreObsoleteProperties作用于接口的请求实体和响应实体参数中的属性。
方法调用等价于:
options.SchemaGeneratorOptions.IgnoreObsoleteProperties = true;
OrderActionsBy
OrderActionsBy用于同一组接口(可以理解为同一控制器下的接口)的排序,默认情况下,一般都是按接口所在类的位置进行排序(源码中是按控制器名称排序,但是同一个控制器中的接口是一样的)。
比如上面的例子中,我们可以修改成按接口路由长度排序:
options.OrderActionsBy(apiDescription => apiDescription.RelativePath.Length.ToString());
运行后Get接口和Post接口就在All接口前面了:
需要注意的是,OrderActionsBy提供的排序只有升序,其实也就是调用IEnumerable<ApiDescription>的OrderBy方法,虽然不理解为什么只有升序,但降序也是可以采用这个升序实现的,将就着用吧。
CustomSchemaIds
CustomSchemaIds方法用于自定义SchemaId,Swashbuckle中的每个Schema都有唯一的Id,框架会使用这个Id匹配引用类型,因此这个Id不能重复。
默认情况下,这个Id是根据类名得到的(不包含命名空间),因此,当我们有两个相同名称的类时,Swashbuckle就会报错:
System.InvalidOperationException: Can't use schemaId "$XXXXX" for type "$XXXX.XXXX". The same schemaId is already used for type "$XXXX.XXXX.XXXX"
就是类似上面的异常,一般时候我们都得去改类名,有点不爽,这时就可以使用这个方法自己自定义实现SchemaId的获取,比如,我们自定义实现使用类名的全限定名(包含命名空间)来生成SchemaId,上面的异常就没有了:
options.CustomSchemaIds(CustomSchemaIdSelector); string CustomSchemaIdSelector(Type modelType) { if (!modelType.IsConstructedGenericType) return modelType.FullName.Replace("[]", "Array"); var prefix = modelType.GetGenericArguments() .Select(genericArg => CustomSchemaIdSelector(genericArg)) .Aggregate((previous, current) => previous + current); return prefix + modelType.FullName.Split('`').First(); }
TagActionsBy
Tag是标签组,也就是将接口做分类的一个概念。
TagActionsBy用于获取一个接口所在的标签分组,默认的接口标签分组是控制器名,也就是接口被分在它所属的控制器下面,我们可以改成按请求方法进行分组
options.TagActionsBy(apiDescription => new string[] { apiDescription.HttpMethod});
运行后:
注意到,上面还有一个Home空标签,如果不想要这个空标签,可以将它的注释去掉,(不明白为什么Swashbuckle为什么空标签也要显示出来,难道是因为作者想着只要有东西能展示,就应该显示出来?)
MapType
MapType用于自定义类型结构(Schema)的生成,Schema指的是接口参数和返回值等的结构信息。
比如,我有一个获取用户信息的接口:
/// <summary> /// 获取用户 /// </summary> /// <returns>用户信息</returns> [HttpGet("GetUser")] public User GetUser(int id) { //这里根据Id获取用户信息 return new User() { Name = "张三" }; }
其中User是自己定义的一个实体
/// <summary> /// 用户信息 /// </summary> public class User { /// <summary> /// 用户名称 /// </summary> public string Name { get; set; } /// <summary> /// 用户密码 /// </summary> public string Password { get; set; } /// <summary> /// 手机号码 /// </summary> public string Phone { get; set; } /// <summary> /// 工作 /// </summary> public string Job { get; set; } }
默认情况下,swagger生成的结构是json格式:
通过MapType方法,可以修改User生成的架构,比如修改成字符串类型:
options.MapType<User>(() => { return new OpenApiSchema() { Type= "string" }; });
运行后显示:
AddServer
Server指的是接口访问的域名和前缀(虚拟路径),以方便访问不同地址的接口(注意设置跨域).
AddServer用于全局的添加接口域名和前缀(虚拟路径)部分信息,默认情况下,如果我们在SwaggerUi页面使用Try it out去调用接口时,默认使用的是当前swaggerUI页面所在的地址域名信息:
而AddServer方法运行我们添加其他的地址域名,比如:
options.AddServer(new OpenApiServer() { Url = "http://localhost:5000", Description = "地址1" }); options.AddServer(new OpenApiServer() { Url = "http://127.0.0.1:5001", Description = "地址2" }); //192.168.28.213是我本地IP options.AddServer(new OpenApiServer() { Url = "http://192.168.28.213:5002", Description = "地址3" });
我分别在上面3个端口开启程序,运行后:
注意:如果读者本地访问不到,看看自己程序是否有监听这三个地址,而且记得要设置跨域,否则会导致请求失败:
public void ConfigureServices(IServiceCollection services) { ...
services.AddCors();
... }
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
...
app.UseCors(builder =>
{
builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
});
...
}
在开发过程中,我们的程序可能会发布到不同的环境,比如本地开发环境,测试环境,预生产环境等等,因此,我们可以使用AddServer方法将不同环境的地址配置上去就能直接实现调用了。
在项目部署时,可能会涉及到虚拟目录之类的东西,比如,使用IIS部署时,可能会给项目加一层虚拟路径:
或者使用nginx做一层反向代理:
这个时候虽然可以使用http://ip:port/Swashbuckle/swagger/index.html访问到swaggerUI,但是此时可能会报错 Not Found /swagger/v1/swagger.json:
这是因为加了虚拟路径,而swagger并不知道,所以再通过/swagger/v1/swagger.json去获取接口架构信息当然会报404了,我们可以改下Swagger中间件:
app.UseSwagger(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/Swashbuckle/swagger/v1/swagger.json", "v1"); options.SwaggerEndpoint("/Swashbuckle/swagger/v2/swagger.json", "v2"); });
再使用虚拟路径就可以访问到SwaggerUI页面了,但是问题还是有的,因为所有接口都没有加虚拟路径,上面说道,swagger调用接口默认是使用SwaggerUI页面的地址+接口路径去访问的,这就会少了虚拟路径,访问自然就变成了404:
这个时候就可以调用AddServer方法去添加虚拟路径了:
//注意下面的端口,已经变了
options.AddServer(new OpenApiServer() { Url = "http://localhost:90/Swashbuckle", Description = "地址1" }); options.AddServer(new OpenApiServer() { Url = "http://127.0.0.1:90/Swashbuckle", Description = "地址2" }); //192.168.28.213是我本地IP options.AddServer(new OpenApiServer() { Url = "http://192.168.28.213:90/Swashbuckle", Description = "地址3" });
部署运行后就可以访问了:
一般的,开发过程中,我们可以把这个虚拟路径做成配置,在然后从配置读取即可。
注:我记得Swashbuckle在swagger2.0的版本中SwaggerDocument中有个BasePath,可以很轻松的设置虚拟路径,但是在swagger3+之后把这个属性删除了,不知道什么原因
AddSecurityDefinition
AddSecurityDefinition用于声明一个安全认证,注意,只是声明,并未指定接口必须要使用认证,比如声明JwtBearer认证方式:
//定义JwtBearer认证方式一 options.AddSecurityDefinition("JwtBearer", new OpenApiSecurityScheme() { Description = "这是方式一(直接在输入框中输入认证信息,不需要在开头添加Bearer)", Name = "Authorization",//jwt默认的参数名称 In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中) Type = SecuritySchemeType.Http, Scheme = "bearer" });
AddSecurityDefinition方法需要提供一个认证名以及一个OpenApiSecurityScheme对象,而这个OpenApiSecurityScheme对象就是描述的认证信息,常用的有:
Type:表示认证方式,有ApiKey,Http,OAuth2,OpenIdConnect四种,其中ApiKey是用的最多的。
Description:认证的描述
Name:携带认证信息的参数名,比如Jwt默认是Authorization
In:表示认证信息发在Http请求的哪个位置
Scheme:认证主题,只对Type=Http生效,只能是basic和bearer
BearerFormat::Bearer认证的数据格式,默认为Bearer Token(中间有一个空格)
Flows:OAuth认证相关设置,比如认证方式等等
OpenIdConnectUrl:使用OAuth认证和OpenIdConnect认证的配置发现地址
Extensions:认证的其他拓展,如OpenIdConnect的Scope等等
Reference:关联认证
这些属性中,最重要的当属Type,它指明了认证的方式,用通俗的话讲:
ApiKey表示就是提供一个框,你填值之后调用接口,会将填的值与Name属性指定的值组成一个键值对,放在In参数指定的位置通过http传送到后台。
Http也是提供了一个框,填值之后调用接口,会将填的值按照Scheme指定的方式进行处理,再和Name属性组成一个键值对,放在In参数指定的位置通过http传送到后台。这也就解释了为什么Bearer认证可以有两种方式。
OAuth2,OpenIdConnect需要提供账号等信息,然后去远程服务进行授权,一般使用Swagger都不推荐使用这种方式,因为比较复杂,而且授权后的信息也可以通过ApiKey方式传送到后台。
再举个例子,比如我们使用Cookie认证:
options.AddSecurityDefinition("Cookies", new OpenApiSecurityScheme() { Description = "这是Cookie认证方式", Name = "Cookies",//这个是Cookie名 In = ParameterLocation.Cookie,//信息保存在Cookie中 Type = SecuritySchemeType.ApiKey });
注:如果将信息放在Cookie,那么在SwaggerUI中调用接口时,认证信息可能不会被携带到后台,因为浏览器不允许你自己操作Cookie,因此在发送请求时会过滤掉你自己设置的Cookie,但是SwaggerUI页面调用生成的Curl命令语句是可以成功访问的
好了,言归正传,当添加了上面JwtBearer认证方式后,这时SwaggerUI多了一个认证的地方:
但是这时调用接口并不需要认证信息,因为还没有指定哪些接口需要认证信息
AddSecurityRequirement
AddSecurityDefinition仅仅是声明已一个认证,不一定要对接口用,而AddSecurityRequirement是将声明的认证作用于所有接口(AddSecurityRequirement好像可以声明和引用一起实现),比如将上面的JwtBearer认证作用于所有接口:
//声明一个Scheme,注意下面的Id要和上面AddSecurityDefinition中的参数name一致 var scheme = new OpenApiSecurityScheme() { Reference = new OpenApiReference() { Type = ReferenceType.SecurityScheme, Id = "JwtBearer" } }; //注册全局认证(所有的接口都可以使用认证) options.AddSecurityRequirement(new OpenApiSecurityRequirement() { [scheme] = new string[0] });
运行后,发现所有接口后面多了一个锁,表明此接口需要认证信息:
AddSecurityRequirement调用需要一个OpenApiSecurityRequirement对象,他其实是一个字典型,也就是说可以给接口添加多种认证方式,而它的键是OpenApiSecurityScheme对象,比如上面的例子中将新定义的OpenApiSecurityScheme关联到已经声明的认证上,而值是一个字符串数组,一般指的是OpenIdConnect的Scope。
需要注意的是,AddSecurityRequirement声明的作用是对全部的接口生效,也就是说所有接口后面都会加锁,但这并不影响我们接口的调用,毕竟调用逻辑还是由后台代码决定的,但是这里加锁就容易让人误导以为都需要认证。
DocumentFilter
document顾名思义,当然指的就是swagger文档了。
DocumentFilter是文档过滤器,它是在获取swagger文档接口,返回结果前调用,也就是请求swagger.json时调用,它允许我们对即将返回的swagger文档信息做调整,比如上面的例子中添加的全局认证方式和AddSecurityRequirement添加的效果是一样的:
public class MyDocumentFilter : IDocumentFilter { public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { //声明一个Scheme,注意下面的Id要和上面AddSecurityDefinition中的参数name一致 var scheme = new OpenApiSecurityScheme() { Reference = new OpenApiReference() { Type = ReferenceType.SecurityScheme, Id = "JwtBearer" } }; //注册全局认证(所有的接口都可以使用认证) swaggerDoc.SecurityRequirements.Add(new OpenApiSecurityRequirement() { [scheme] = new string[0] }); } }
然后使用DocumentFilter方法添加过滤器:
options.DocumentFilter<MyDocumentFilter>();
DocumentFilter方法需要提供一个实现了IDocumentFilter接口的Apply方法的类型和它实例化时所需要的的参数,而IDocumentFilter的Apply方法提供了OpenApiDocument和DocumentFilterContext两个参数,DocumentFilterContext参数则包含了当前文件接口方法的信息,比如调用的接口的Action方法和Action的描述(如路由等)。而OpenApiDocument即包含当前请求的接口文档信息,它包含的属性全部都是全局性的, 这样我们可以像上面添加认证一样去添加全局配置,比如,如果不使用AddServer方法,我们可以使用DocumentFilter去添加:
public class MyDocumentFilter : IDocumentFilter { public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { swaggerDoc.Servers.Add(new OpenApiServer() { Url = "http://localhost:90", Description = "地址1" }); swaggerDoc.Servers.Add(new OpenApiServer() { Url = "http://127.0.0.1:90", Description = "地址2" }); //192.168.28.213是我本地IP swaggerDoc.Servers.Add(new OpenApiServer() { Url = "http://192.168.28.213:90", Description = "地址3" }); } }
记得使用DocumentFilter添加过滤器。
再比如,上面我们对接口进行了swagger文档分类使用的是ApiExplorerSettingsAttribute,如果不想对每个接口使用ApiExplorerSettingsAttribute,我们可以使用DocumentFilter来实现,先创建一个类实现IDocumentFilter接口:
public class GroupNameDocumentFilter : IDocumentFilter { string documentName; string[] actions; public GroupNameDocumentFilter(string documentName, params string[] actions) { this.documentName = documentName; this.actions = actions; } public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) { foreach (var apiDescription in context.ApiDescriptions) { if (actions.Contains(apiDescription.ActionDescriptor.RouteValues["action"])) { apiDescription.GroupName = documentName; } } } }
然后使用DocumentFilter添加过滤器:
//All和Get接口属于文档v1 options.DocumentFilter<GroupNameDocumentFilter>(new object[] { "v1", new string[] { nameof(HomeController.Get) } }); //All和Post接口属于v2 options.DocumentFilter<GroupNameDocumentFilter>(new object[] { "v2", new string[] { nameof(HomeController.Post) } });
然后取消上面Get方法和Post方法的ApiExplorerSettings特性,这样实现的效果和上面直接使用ApiExplorerSettings特性修饰的效果是相似的。
这里说相似并非一致,是因为上面的GroupNameDocumentFilter是在第一次获取swagger.json时执行设置GroupName,也就是说第一次获取swagger.json会获取到所有的接口,所以一般也不会采用这种方法,而是采用上面介绍的使用IActionModelConvention和IControllerModelConvention来实现。
OperationFilter
什么是Operation?Operation可以简单的理解为一个操作,因为swagger是根据项目中的接口,自动生成接口文档,就自然需要对每个接口进行解析,接口路由是什么,接口需要什么参数,接口返回什么数据等等,而对每个接口的解析就可以视为一个Operation。
OperationFilter是操作过滤器,这个方法需要一个实现类IOperationFilter接口的类型,而它的第二个参数arguments是这个类型实例化时传入的参数。
OperationFilter允许我们对已经生成的接口进行修改,比如可以添加参数,修改参数类型等等。
需要注意的是,OperationFilter在获取swagger文档接口时调用,也就是请求swagger.json时调用,而且只对属于当前请求接口文档的接口进行过滤调用。
比如我们有一个Operation过滤器:
public class MyOperationFilter : IOperationFilter { string documentName; public MyOperationFilter(string documentName) { this.documentName = documentName; } public void Apply(OpenApiOperation operation, OperationFilterContext context) { //过滤处理 } }
接着调用SwaggerGenOptions的OperationFilter方法添加
options.OperationFilter<MyOperationFilter>(new object[] { "v1" });
上面的过滤器实例化需要一个参数documentName,所以在OperationFilter方法中有一个参数。
这个接口只会对当前请求的接口文档进行调用,也就是说,如果我们请求的是swagger文档v1,也就是请求/swagger/v1/swagger.json时,这个过滤器会对All方法和Get方法执行,如果请求的是swagger文档v2,也就是请求/swagger/v2/swagger.json时,这个过滤器会对All方法和Post方法进行调用。自定义的OperationFilter需要实现IOperationFilter的Apply接口方法,而Apply方法有两个参数:OpenApiOperation和OperationFilterContext,同样的,OpenApiOperation包含了和当前接口相关的信息,比如认证情况,所属的标签,还可以自定义的自己的Servers。而OperationFilterContext则包换了接口方法的的相关引用。
OperationFilter是用的比较多的方法了,比如上面的全局认证,因为直接调用AddSecurityRequirement添加的是全局认证,但是项目中可能部分接口不需要认证,这时我们就可以写一个OperationFilter对每一个接口进行判断了:
public class ResponsesOperationFilter : IOperationFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { var authAttributes = context.MethodInfo.DeclaringType.GetCustomAttributes(true) .Union(context.MethodInfo.GetCustomAttributes(true)) .OfType<AuthorizeAttribute>(); var list = new List<OpenApiSecurityRequirement>(); if (authAttributes.Any() && !context.MethodInfo.GetCustomAttributes(true).OfType<AllowAnonymousAttribute>().Any()) { operation.Responses["401"] = new OpenApiResponse { Description = "Unauthorized" }; //operation.Responses.Add("403", new OpenApiResponse { Description = "Forbidden" }); //声明一个Scheme,注意下面的Id要和AddSecurityDefinition中的参数name一致 var scheme = new OpenApiSecurityScheme() { Reference = new OpenApiReference() { Type = ReferenceType.SecurityScheme, Id = "JwtBearer" } }; //注册全局认证(所有的接口都可以使用认证) operation.Security = new List<OpenApiSecurityRequirement>(){new OpenApiSecurityRequirement() { [scheme] = new string[0] }}; } } }
然后使用OperationFilter添加这个过滤器:
options.OperationFilter<ResponsesOperationFilter>();
现在可以测试一下了,我们将上面的All接口使用Authorize特性添加认证
/// <summary> /// 未使用ApiExplorerSettings特性,表名属于每一个swagger文档 /// </summary> /// <returns>结果</returns> [HttpGet("All"), Authorize] public string All() { return "All"; }
然后运行项目得到:
再比如,我们一般写接口,都会对返回的数据做一个规范,比如每个接口都会有响应代码,响应信息等等,而程序中我们是通过过滤器去实现的,所以接口都是直接返回数据,但是我们的swagger不知道,比如上面我们的测试接口返回的都是string类型,所以页面上也是展示string类型没错:
假如我们添加了过滤器对结果进行了一个处理,结果不在是string类型了,这个时候我们就可以使用OperationFilter做一个调整了:
public class MyOperationFilter : IOperationFilter { public void Apply(OpenApiOperation operation, OperationFilterContext context) { foreach (var key in operation.Responses.Keys) { var content = operation.Responses[key].Content; foreach (var mediaTypeKey in content.Keys) { var mediaType = content[mediaTypeKey]; var schema = new OpenApiSchema(); schema.Type = "object"; schema.Properties = new Dictionary<string, OpenApiSchema>() { ["code"] = new OpenApiSchema() { Type = "integer" }, ["message"] = new OpenApiSchema() { Type = "string" }, ["error"] = new OpenApiSchema() { Type = "object", Properties = new Dictionary<string, OpenApiSchema>() { ["message"] = new OpenApiSchema() { Type = "string" }, ["stackTrace"] = new OpenApiSchema() { Type = "string" } } }, ["result"] = mediaType.Schema }; mediaType.Schema = schema; } } } }
记得使用OperationFilter添加过滤器:
options.OperationFilter<MyOperationFilter>();
显示效果如下:
RequestBodyFilter
RequestBody理所当然的就是请求体了,一般指的就是Post请求,RequestBodyFilter就是允许我们对请求体的信息作出调整,同样的,它是在获取Swagger.json文档时调用,而且只对那些有请求体的接口才会执行。
RequestBodyFilter的用法类似DocumentFilter和OperationFilter,一般也不会去修改请求体的默认行为,因为它可能导致请求失败,所以一般不常用,这里就不介绍了
ParameterFilter
Parameter指的是接口的参数,而ParameterFilter当然就是允许我们对参数的结构信息作出调整了,同样的,它是在获取Swagger.json文档时调用,而且只对那些参数的接口才会执行。
比如,我们有这么一个接口:
/// <summary> /// 有参数接口 /// </summary> /// <returns></returns> [HttpGet("GetPara")] public string GetPara(string para="default") { return $"para is {para},but para from header is {Request.Headers["para"]}"; }
然后我们可以使用ParameterFilter修改上面para参数在http请求中的位置,比如将它放在请求头中:
public class MyParameterFilter : IParameterFilter { public void Apply(OpenApiParameter parameter, ParameterFilterContext context) { if (context.ParameterInfo.Name == "para") { parameter.In = ParameterLocation.Header; } } }
然后使用ParameterFilter方法添加过滤器:
options.ParameterFilter<MyParameterFilter>();
运行后:
不过一般不会使用ParameterFilter去修改参数的默认行为,因为这可能会导致接口调用失败。
SchemaFilter
Schema指的是结构,一般指的是接口请求参数和响应返回的参数结构,比如我们想将所有的int类型换成string类型:
public class MySchemaFilter : ISchemaFilter { public void Apply(OpenApiSchema schema, SchemaFilterContext context) { if (context.Type == typeof(int)) { schema.Type = "string"; } } }
假如有接口:
/// <summary> /// 测试接口 /// </summary> /// <returns></returns> [HttpGet("Get")] public int Get(int id) { return 1; }
运行后所有的int参数在swaggerUI上都会显示为string 类型:
再比如,我们可以使用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中体现出来,这个时候我们可以通过SchemaFilter来修改Schema信息。
比如,可以先用一个特性(例如使用DescriptionAttribute)标识枚举类型的每一项,用于说明含义:
public enum SexEnum { /// <summary> /// 未知 /// </summary> [Description("未知")] Unknown = 0, /// <summary> /// 男 /// </summary> [Description("男")] Male = 1, /// <summary> /// 女 /// </summary> [Description("女")] Female = 2 }
接着我们创建一个MySchemaFilter类,实现ISchemaFilter接口:
public class MySchemaFilter : ISchemaFilter { static readonly ConcurrentDictionary<Type, Tuple<string, object>[]> dict = new ConcurrentDictionary<Type, Tuple<string, object>[]>(); public void Apply(OpenApiSchema schema, SchemaFilterContext context) { 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, context); } } private void UpdateSchemaDescription(OpenApiSchema schema, SchemaFilterContext context) { if (schema.Reference!=null) { var s = context.SchemaRepository.Schemas[schema.Reference.Id]; if (s != null && s.Enum != null && s.Enum.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, context); } } /// <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中使用
services.AddSwaggerGen(options => { ... options.SchemaFilter<MySchemaFilter>(); });
再次运行项目后,得到的架构就有每个枚举项的属性了,当然,你也可以安装自己的意愿去生成特定格式的架构,这只是一个简单的例子
其他方法
其他方法就不准备介绍了,比如:
DescribeAllEnumsAsStrings方法表示在将枚举类型解释成字符串名称而不是默认的整形数字
DescribeAllParametersInCamelCase方法表示将参数使用驼峰命名法处理
等等这些方法都用的比较少,而且这些都比较简单,感兴趣的可以看看源码学习
另外需要注意的是,在Swashbuckle.AspNetCore 6.0+以后的版本中,上面两个方法已经被移除了,作者希望我们通过.net core提供的依赖注入及JsonConverter机制自行去实现。
但是作者有提供了一个 Swashbuckle.AspNetCore.Newtonsoft 包,基于Newtonsoft.Json 来实现DescribeAllEnumsAsStrings,DescribeAllParametersInCamelCase 原来的这两个方法:
services.AddSwaggerGenNewtonsoftSupport(); services.Configure<MvcNewtonsoftJsonOptions>(options => { //等价于原来的DescribeAllEnumsAsStrings方法 options.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter()); //等价于原来的DescribeAllParametersInCamelCase方法 options.SerializerSettings.Converters.Add(new Newtonsoft.Json.Converters.StringEnumConverter(new Newtonsoft.Json.Serialization.CamelCaseNamingStrategy())); });
特别注意的是,这样做是解决Swagger页面展示枚举类型时按字符串展示,但真实调用接口返回的格式还是需要自行实现JsonConverter。
毕竟Swagger只是接口说明文档,它不影响真实接口返回的数据信息,而.net core的MVC序列化有两种方案:Newtonsoft.Json和System.Text.Json,所以这也是预料之中的事。
三、添加Swagger中间件(UseSwagger,UseSwaggerUI)
细心地朋友应该注意到,在上面的例子中,添加Swagger中间件其实有两个,分别是UseSwagger和UseSwaggerUI两个方法:
UseSwagger:添加Swagger中间件,主要用于拦截swagger.json请求,从而可以获取返回所需的接口架构信息
UseSwaggerUI:添加SwaggerUI中间件,主要用于拦截swagger/index.html页面请求,返回页面给前端
整个swagger页面访问流程如下:
1、浏览器输入swaggerUI页面地址,比如:http://localhost:5000/swagger/index.html,这个地址是可配置的
2、请求被SwaggerUI中间件拦截,然后返回页面,这个页面是嵌入的资源文件,也可以设置成外部自己的页面文件(使用外部静态文件拦截)
3、页面接收到Swagger的Index页面后,会根据SwaggerUI中间件中使用SwaggerEndpoint方法设置的文档列表,加载第一个文档,也就是获取文档架构信息swagger.json
4、浏览器请求的swagger.json被Swagger中间件拦截,然后解析属于请求文档的所有接口,并最终返回一串json格式的数据
5、浏览器根据接收到的swagger,json数据呈现UI界面
UseSwagger方法有个包含SwaggerOptions的重载,UseSwaggerUI则有个包含SwaggerUIOptions的重载,两者相辅相成,所以这里在一起介绍这两个方法
SwaggerOptions
SwaggerOptions比较简单,就三个属性:
RouteTemplate
路由模板,默认值是/swagger/{documentName}/swagger.json,这个属性很重要!而且这个属性中必须包含{documentName}参数。
上面第3、4步骤已经说到,index.html页面会根据SwaggerUI中间件中使用SwaggerEndpoint方法设置的文档列表,然后使用第一个文档的路由发送一个GET请求,请求会被Swagger中间件中拦截,然后Swagger中间件中会使用RouteTemplate属性去匹配请求路径,然后得到documentName,也就是接口文档名,从而确定要返回哪些接口,所以,这个RouteTemplate一定要配合SwaggerEndpoint中的路由一起使用,要保证通过SwaggerEndpoint方法中的路由能找到documentName。
比如,如果将RouteTemplate设置成:
app.UseSwagger(options => { options.RouteTemplate = "/{documentName}.json"; });
那么SwaggerEndpoint就得做出相应的调整:
app.UseSwaggerUI(options => { options.SwaggerEndpoint("/v1.json", "v1"); options.SwaggerEndpoint("/v2.json", "v2"); });
当然,上面的SwaggerEndpoint方法中的路由可以添加虚拟路径,毕竟虚拟路径会在转发时被处理掉。
总之,这个属性很重要,尽可能不要修改,然后是上面默认的格式在SwaggerEndpoint方法中声明。
SerializeAsV2
表示按Swagger2.0格式序列化生成swagger.json,这个不推荐使用,尽可能的使用新版本的就可以了。
PreSerializeFilters
这个属性也是个过滤器,类似于上面介绍的DocumentFilter,在解析完所有接口后得到swaggerDocument之后调用执行,也就是在DocumentFilter,OperationFilter等过滤器之后调用执行。不建议使用这个属性,因为它能实现的功能使用DocumentFilter,OperationFilter等过滤器都能实现。
SwaggerUIOptions
SwaggerUIOptions则包含了SwaggerUI页面的一些设置,主要有六个属性:
RoutePrefix
设置SwaggerUI的Index页面的地址,默认是swagger,也就是说可以使用http://host:port/swagger可以访问到SwaggerUI页面,如果设置成空字符串,那么久可以使用http://host:port直接访问到SwaggerUI页面了
IndexStream
上面解释过,Swagger的UI页面是嵌入的资源文件,默认值是:
app.UseSwaggerUI(options => { options.IndexStream = () => typeof(SwaggerUIOptions).GetTypeInfo().Assembly.GetManifestResourceStream("Swashbuckle.AspNetCore.SwaggerUI.index.html"); });
我们可以修改成自己的页面,比如Hello World:
app.UseSwaggerUI(options => { options.IndexStream = () => new MemoryStream(Encoding.UTF8.GetBytes("Hello World")); });
DocumentTitle
这个其实就是html页面的title
HeadContent
这个属性是往SwaggerUI页面head标签中添加我们自己的代码,比如引入一些样式文件,或者执行自己的一些脚本代码,比如:
app.UseSwaggerUI(options => { options.HeadContent += $"<script type='text/javascript'>alert('欢迎来到SwaggerUI页面')</script>"; });
然后进入SwaggerUI就会弹出警告框了。
注意,上面的设置使用的是+=,而不是直接赋值。
但是一般时候,我们不是直接使用HeadConten属性的,而是使用 SwaggerUIOptions的两个拓展方法去实现:InjectStylesheet和InjectJavascript,这两个拓展方法主要是注入样式和javascript代码:
/// <summary> /// Injects additional CSS stylesheets into the index.html page /// </summary> /// <param name="options"></param> /// <param name="path">A path to the stylesheet - i.e. the link "href" attribute</param> /// <param name="media">The target media - i.e. the link "media" attribute</param> public static void InjectStylesheet(this SwaggerUIOptions options, string path, string media = "screen") { var builder = new StringBuilder(options.HeadContent); builder.AppendLine($"<link href='{path}' rel='stylesheet' media='{media}' type='text/css' />"); options.HeadContent = builder.ToString(); } /// <summary> /// Injects additional Javascript files into the index.html page /// </summary> /// <param name="options"></param> /// <param name="path">A path to the javascript - i.e. the script "src" attribute</param> /// <param name="type">The script type - i.e. the script "type" attribute</param> public static void InjectJavascript(this SwaggerUIOptions options, string path, string type = "text/javascript") { var builder = new StringBuilder(options.HeadContent); builder.AppendLine($"<script src='{path}' type='{type}'></script>"); options.HeadContent = builder.ToString(); }
ConfigObject
其他配置对象,包括之前介绍的SwaggerDocument文档的地址等等。
OAuthConfigObject
和OAuth认证有关的配置信息,比如ClientId、ClientSecret等等。
对于ConfigObject,OAuthConfigObject两个对象,一般都不是直接使用它,而是用SwaggerUIOptions的拓展方法,比如之前一直介绍的SwaggerEndpoint方法,其实就是给ConfigObject的Urls属性增加对象:
/// <summary> /// Adds Swagger JSON endpoints. Can be fully-qualified or relative to the UI page /// </summary> /// <param name="options"></param> /// <param name="url">Can be fully qualified or relative to the current host</param> /// <param name="name">The description that appears in the document selector drop-down</param> public static void SwaggerEndpoint(this SwaggerUIOptions options, string url, string name) { var urls = new List<UrlDescriptor>(options.ConfigObject.Urls ?? Enumerable.Empty<UrlDescriptor>()); urls.Add(new UrlDescriptor { Url = url, Name = name} ); options.ConfigObject.Urls = urls; }
四、总结
到这里基本上就差不多了,写了这么多该收尾了。
主要就是记住三点:
1、服务注入使用AddSwaggerGen方法,主要就是生成接口相关信息,如认证,接口注释等等,还有几种过滤器帮助我们实现自己的需求
2、中间件注入有两个:UseSwagger和UseSwaggerUI:
UseSwagger负责返回接口架构信息,返回的是json格式的数据
UseSwaggerUI负责返回的是页面信息,返回的是html内容
3、如果涉及到接口生成的,尽可能在AddSwaggerGen中实现,如果涉及到UI页面的,尽可能在UseSwaggerUI中实现