由ASP.NET Core WebApi添加Swagger报错引发的探究
缘起
在使用ASP.NET Core进行WebApi项目开发的时候,相信很多人都会使用Swagger作为接口文档呈现工具。相信大家也用过或者了解过Swagger,这里咱们就不过多的介绍了。本篇文章记录一下,笔者在使用ASP.NET Core开发Api的过程中,给接口整合Swagger过程中遇到的一个异常,笔者抱着好奇的心态研究了一下异常的原因,并解决了这个问题。在这个过程中笔者学到了一些新的技能,得到了一些新的知识,便打算记录一下,希望能帮助到更多的人。
示例
从项目渊源上说起,笔者所在项目,很多都是从.Net FrameWork的老项目迁移到ASP.NET Core上来的,这其中做了很多兼容的处理,来保证尽量不修改原有的业务代码,这其中就包含了WebApi相关的部分,这里我们用简单的示例描述现有WebApi的Controller的情况,大致写法如下
[Route("api/[controller]/[action]")]
[ApiController]
public class OrderController : ControllerBase
{
private List<OrderDto> orderDtos = new List<OrderDto>();
public OrderController()
{
orderDtos.Add(new OrderDto { Id = 1,TotalMoney=222,Address="北京市",Addressee="me",From="淘宝",SendAddress="武汉" });
orderDtos.Add(new OrderDto { Id = 2, TotalMoney = 111, Address = "北京市", Addressee = "yi", From = "京东", SendAddress = "北京" });
orderDtos.Add(new OrderDto { Id = 3, TotalMoney = 333, Address = "北京市", Addressee = "yi念之间", From = "天猫", SendAddress = "杭州" });
}
/// <summary>
/// 获取订单数据
/// </summary>
public OrderDto Get(long id)
{
return orderDtos.FirstOrDefault(i => i.Id == id);
}
/// <summary>
/// 添加订单数据
/// </summary>
public IActionResult Add(OrderDto orderDto)
{
orderDtos.Add(orderDto);
return Ok();
}
/// <summary>
/// 添加订单数据
/// </summary>
public IActionResult Edit(long id, OrderDto orderDto)
{
var order = orderDtos.FirstOrDefault(i => i.Id == id);
if (order == null)
{
return NotFound();
}
order.Address = orderDto.Address;
order.From = orderDto.From;
return Ok();
}
/// <summary>
/// 删除订单数据
/// </summary>
public IActionResult Delete(long id)
{
var order = orderDtos.FirstOrDefault(i=>i.Id==id);
if (order == null)
{
return NotFound();
}
orderDtos.Remove(order);
return Ok();
}
}
虽然是笔者写的demo,但是大致是这种形式,而且直接通过ASP.NET Core运行起来也没有任何的问题,调用也不会出现任何异常。当项目开发完成后,给项目添加Swagger,笔者用的是Swashbuckle.AspNetCore这个组件,添加Swagger的方式大致如下,首先是在Startup类的ConfigureServices方法中添加以下代码
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "OrderApi",
Description = "订单服务接口"
});
var xmlCommentFile = $"{AppContext.BaseDirectory}OrderApi.xml";
if (File.Exists(xmlCommentFile))
{
c.IncludeXmlComments(xmlCommentFile);
}
});
添加完成之后,在Configure方法中开启Swagger中间件,具体代码如下
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "OrderApi");
});
添加完成之后,运行起来项目打开Swagger地址http://localhost:5000/swagger结果直接弹出了一个红色浮窗,看样子有异常,打开.Net Core控制台窗口看到了如下异常
fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1] An unhandled exception has occurred while executing the request.
Swashbuckle.AspNetCore.SwaggerGen.SwaggerGeneratorException: Ambiguous HTTP method for action OrderApi.Controllers.OrderController.Get (OrderApi).
Actions require an explicit HttpMethod binding for Swagger/OpenAPI 3.0
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateOperations(IEnumerable`1 apiDescriptions, SchemaRepository schemaRepository)
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GeneratePaths(IEnumerable`1 apiDescriptions, SchemaRepository schemaRepository)
at Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GetSwagger(String documentName, String host, String basePath)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
其中核心的关键词汇就是Ambiguous HTTP method for action OrderApi.Controllers.OrderController.Get (OrderApi). Actions require an explicit HttpMethod binding for Swagger/OpenAPI 3.0笔者用尽毕生的英语修为,了解到其大概意思是Swagger/OpenAPI 3.0要求Action上必须绑定HttpMethod相关Attribute,否则就报这一大堆错误。这里的HttpMethod其实就是咱们常用HttpGet、HttpPost、HttpPut、HttpDelete相关的Attribute。
正常逻辑来说那就给每个Action添加HttpMethod呗,但是往往情况就出现在不正常的时候。因为项目是迁移的老项目,先不说私自改了别人代码带来的甩锅问题,公司的WebApi项目很多,这意味着Action很多,如果一个项目一个项目的去找Action添加HttpMethod可是一个不小的工作量,而且开发人员工作繁忙,基本上不会抽出来时间去修改这些的,因为这种只是Swagger不行,但是对于WebApi本身来说这种写法没有任何的问题,也不会报错,只是看起来不规范。那该怎么办呢?
探究源码
又看了看异常决定从源码入手,通过控制台报出的异常可以看到报错的最初位置是在Swashbuckle.AspNetCore.SwaggerGen.SwaggerGenerator.GenerateOperations(IEnumerable1 apiDescriptions, SchemaRepository schemaRepository)`那就从这里准备入手了。
Swashbuckle.AspNetCore入手
在GitHub上找到Swashbuckle.AspNetCore仓库位置,近期GitHub不太稳定,除了梯子貌似也没有很好的办法,多刷新几次将就着用吧,由异常信息可知抛出异常所在的位置SwaggerGenerator类的GenerateOperations方法直接找到源码位置[点击查看源码]代码如下
private IDictionary<OperationType, OpenApiOperation> GenerateOperations(IEnumerable<ApiDescription> apiDescriptions,
SchemaRepository schemaRepository)
{
//根据HttpMethod分组
var apiDescriptionsByMethod = apiDescriptions
.OrderBy(_options.SortKeySelector)
.GroupBy(apiDesc => apiDesc.HttpMethod);
var operations = new Dictionary<OperationType, OpenApiOperation>();
foreach (var group in apiDescriptionsByMethod)
{
var httpMethod = group.Key;
if (httpMethod == null)
//异常位置在这里
throw new SwaggerGeneratorException(string.Format(
"Ambiguous HTTP method for action - {0}. " +
"Actions require an explicit HttpMethod binding for Swagger/OpenAPI 3.0",
group.First().ActionDescriptor.DisplayName));
if (group.Count() > 1 && _options.ConflictingActionsResolver == null)
throw new SwaggerGeneratorException(string.Format(
"Conflicting method/path combination \"{0} {1}\" for actions - {2}. " +
"Actions require a unique method/path combination for Swagger/OpenAPI 3.0. Use ConflictingActionsResolver as a workaround",
httpMethod,
group.First().RelativePathSansQueryString(),
string.Join(",", group.Select(apiDesc => apiDesc.ActionDescriptor.DisplayName))));
var apiDescription = (group.Count() > 1) ? _options.ConflictingActionsResolver(group) : group.Single();
operations.Add(OperationTypeMap[httpMethod.ToUpper()], GenerateOperation(apiDescription, schemaRepository));
};
return operations;
}
httpMethod属性的数据源来自IEnumerable<ApiDescription>集合,顺着调用关系往上找,最后发现ApiDescription来自IApiDescriptionGroupCollectionProvider而它来自于构造函数注入进来的
private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionsProvider;
private readonly ISchemaGenerator _schemaGenerator;
private readonly SwaggerGeneratorOptions _options;
public SwaggerGenerator(
SwaggerGeneratorOptions options,
IApiDescriptionGroupCollectionProvider apiDescriptionsProvider,
ISchemaGenerator schemaGenerator)
{
_options = options ?? new SwaggerGeneratorOptions();
_apiDescriptionsProvider = apiDescriptionsProvider;
_schemaGenerator = schemaGenerator;
}
看名字也知道IApiDescriptionGroupCollectionProvider是专门服务于Api描述相关的,在Swashbuckle.AspNetCore仓库中造了下没发现相关定义,于是用VS找到引用发现定义如下
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{
public interface IApiDescriptionGroupCollectionProvider
{
ApiDescriptionGroupCollection ApiDescriptionGroups { get; }
}
}
转战aspnetcore
看命名空间IApiDescriptionGroupCollectionProvider居然是AspNetCore.Mvc下的,也就是说来自AspNetCore自身,跑到AspNetCore的核心仓库搜索了一下代码找到如下位置代码[点击查看源码]
internal static void AddApiExplorerServices(IServiceCollection services)
{
services.TryAddSingleton<IApiDescriptionGroupCollectionProvider, ApiDescriptionGroupCollectionProvider>();
services.TryAddEnumerable(
ServiceDescriptor.Transient<IApiDescriptionProvider, DefaultApiDescriptionProvider>());
}
而AddApiExplorerServices方法是在当前类的AddApiExplorer扩展方法中被调用的
public static IMvcCoreBuilder AddApiExplorer(this IMvcCoreBuilder builder)
{
AddApiExplorerServices(builder.Services);
return builder;
}
看到IMvcCoreBuilder接口,我们就应该感觉到这是Mvc的核心接口扩展方法,但是趋于好奇心还是往上找了一下,发现确实是跟着ASP.NET Core土生土长的实现,最终位置如下[点击查看源码]
private static IMvcCoreBuilder AddControllersCore(IServiceCollection services)
{
return services
.AddMvcCore()
.AddApiExplorer()
.AddAuthorization()
.AddCors()
.AddDataAnnotations()
.AddFormatterMappings();
}
微软想的还是比较周到的,居然在ASP.NET Core的核心位置,加入了IApiDescriptionGroupCollectionProvider这种操作,在IApiDescriptionGroupCollectionProvider的示例中包含了当前Api项目有关Controller和Action相关的信息,而Swagger的Doc文档也就是咱们看到的swagger.json正是基于这些数据信息组装而来。
IApiDescriptionGroupCollectionProvider还是比较实用,如果在不知道这个操作存在的情况下,我们获取WebApi的Controller或Action相关的信息,首先想到的就是反射Controller得到这些,如今有了IApiDescriptionGroupCollectionProvider我们可以在IOC容器中直接获取这个接口的实例,获取Controller和Action的信息。
解决问题
我们找到了问题的根源,可以下手解决问题了,其本质问题是Swagger通过ApiDescription获取Action的HttpMethod信息,但是我们项目由于各种原因,在Action上并没有添加HttpMethod相关的Attribute,所以我们只能从ApiDescription入手,好在我们可以在IOC容器中获取到IApiDescriptionGroupCollectionProvider的实例,从这里入手扩展一个方法,具体实现如下
/// <summary>
/// action没有httpmethod attribute的情况下根据action的开头名称给与默认值
/// </summary>
/// <param name="app">IApplicationBuilder</param>
/// <param name="defaultHttpMethod">默认给定的HttpMethod</param>
public static void AutoHttpMethodIfActionNoBind(this IApplicationBuilder app, string defaultHttpMethod = null)
{
//从容器中获取IApiDescriptionGroupCollectionProvider实例
var apiDescriptionGroupCollectionProvider = app.ApplicationServices.GetRequiredService<IApiDescriptionGroupCollectionProvider>();
var apiDescriptionGroupsItems = apiDescriptionGroupCollectionProvider.ApiDescriptionGroups.Items;
//遍历ApiDescriptionGroups
foreach (var apiDescriptionGroup in apiDescriptionGroupsItems)
{
foreach (var apiDescription in apiDescriptionGroup.Items)
{
if (string.IsNullOrEmpty(apiDescription.HttpMethod))
{
//获取Action名称
var actionName = apiDescription.ActionDescriptor.RouteValues["action"];
//默认给定POST
string methodName = defaultHttpMethod ?? "POST";
//根据Action开头单词给定HttpMethod默认值
if (actionName.StartsWith("get", StringComparison.OrdinalIgnoreCase))
{
methodName = "GET";
}
else if (actionName.StartsWith("put", StringComparison.OrdinalIgnoreCase))
{
methodName = "PUT";
}
else if (actionName.StartsWith("delete", StringComparison.OrdinalIgnoreCase))
{
methodName = "DELETE";
}
apiDescription.HttpMethod = methodName;
}
}
}
}
写完上面的代码后,抱着试试看的心情,因为不清楚这波操作好不好使,将扩展方法引入到Configure方法中,为了清晰和Swagger中间件放到一起后,效果如下
if (!env.IsProduction())
{
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "OrderApi");
});
//给没有配置httpmethod的action添加默认操作
app.AutoHttpMethodIfActionNoBind();
}
加完之后重新运行项目,打开swagger地址http://localhost:5000/swagger没有异常,在Swagger上调用了接口试了一下,没有任何问题。这样的话可以做到只添加一个扩展方法就能解决问题,而不需要挨个Action进行添加HttpMethod。如果想需要更智能的判断Action默认的HttpMethod需要如何定位,直接修改AutoHttpMethodIfActionNoBind扩展方法,因为我们WebApi项目的Action大部分调用方式都是HttpPost,所以这里的逻辑我写的比较简单。
后续小插曲
通过上面的方式解决了Swagger报错之后,在后来无意中翻看Swashbuckle.AspNetCore文档的时候发现了IDocumentFilter这个Swagger过滤器,想着如果能通过过滤器的方式去解决这个问题会更优雅。我们都知道过滤器的作用,而这个过滤器通过看名字我们可以知道他是在生成SwaggerDoc的时候可以对Doc数据进行处理,于是尝试写了一个过滤器,实现如下
public class AutoHttpMethodOperationFitler : IDocumentFilter
{
private readonly string _defaultHttpMethod;
public AutoHttpMethodOperationFitler(string defaultHttpMethod = null)
{
_defaultHttpMethod = defaultHttpMethod;
}
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
//通过DocumentFilterContext上下文可以获取到ApiDescription集合
foreach (var apiDescription in context.ApiDescriptions)
{
//为null说明没有给Action添加HttpMethod
if (string.IsNullOrEmpty(apiDescription.HttpMethod))
{
//这些逻辑是和AutoHttpMethodIfActionNoBind扩展方法保持一致的
var actionName = apiDescription.ActionDescriptor.RouteValues["action"];
string methodName = "POST";
if (actionName.StartsWith("get", StringComparison.OrdinalIgnoreCase))
{
methodName = "GET";
}
else if (actionName.StartsWith("put", StringComparison.OrdinalIgnoreCase))
{
methodName = "PUT";
}
else if (actionName.StartsWith("delete", StringComparison.OrdinalIgnoreCase))
{
methodName = "DELETE";
}
apiDescription.HttpMethod = methodName;
}
}
}
}
编写完成之后再AddSwaggerGen方法中注册AutoHttpMethodOperationFitler过滤器,如下所示
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "OrderApi",
Description = "订单服务接口"
});
//这里注册DocumentFilter
c.DocumentFilter<AutoHttpMethodOperationFitler>();
var xmlCommentFile = $"{AppContext.BaseDirectory}OrderApi.xml";
if (File.Exists(xmlCommentFile))
{
c.IncludeXmlComments(xmlCommentFile);
}
});
忙活完这一波之后注释掉AutoHttpMethodOperationFitler扩展方法,添加AutoHttpMethodOperationFitler过滤器,然后运行一波,打开Swagger地址。不过很遗憾还是会报Actions require an explicit HttpMethod binding for Swagger/OpenAPI 3.0这个异常,想了想为啥还会报这个异常无果后,决定还是翻看源码看一下,这一看果然找到了原因,代码如下[点击查看源码]
var swaggerDoc = new OpenApiDocument
{
Info = info,
Servers = GenerateServers(host, basePath),
//出现异常的代码方法在这里被调用
Paths = GeneratePaths(applicableApiDescriptions, schemaRepository),
Components = new OpenApiComponents
{
Schemas = schemaRepository.Schemas,
SecuritySchemes = new Dictionary<string, OpenApiSecurityScheme>(_options.SecuritySchemes)
},
SecurityRequirements = new List<OpenApiSecurityRequirement>(_options.SecurityRequirements)
};
//执行IDocumentFilter Apply方法的地方在这里
var filterContext = new DocumentFilterContext(applicableApiDescriptions, _schemaGenerator, schemaRepository);
foreach (var filter in _options.DocumentFilters)
{
filter.Apply(swaggerDoc, filterContext);
}
通过上面的源码可以看到,针对数据源信息是否规范的校验,是在执行IDocumentFilter过滤器的Apply方法之前进行的,所以我们在DocumentFilter处理HttpMethod的问题是解决不了的。到这里自己也明白了AutoHttpMethodOperationFitler目前是解决这个问题能想到的最好方式,暂时算是没啥遗憾了。
总结
本篇文章讲解了在给ASP.NET Core添加Swagger的时候遇到的一个异常而引发的对相关源码的探究,并最终解决这个问题,这里我们Get到了一个比较实用的技能,ASP.NET Core内置了IApiDescriptionGroupCollectionProvider实现,通过它我们可以很便捷的获取到WebApi中关于Controller和Action的元数据信息,而这些信息方便我们生成帮助文档或者生成调用代码是非常实用的。如果你对源码感兴趣,或者有通过看源码解决问题的意识的话,这种方式还是比较有效的,因为我们作为程序员最懂的还是代码,而代码的报错当然也得看着代码解决。解决这类问题也没啥特别好的技巧,通过异常堆栈找到报错的原始位置,顺序需要用到的代码一步一步的往上找,直到找到源头。而这也正是看源码的乐趣,要么好奇驱使,要么解决问题。更好的理解代码,就有更好的方式解决问题,就比如我没办法挨个给Action添加HttpMethod所以找到另一个途径解决问题。
由ASP.NET Core WebApi添加Swagger报错引发的探究的更多相关文章
- ASP.NET Core WebApi使用Swagger生成api说明文档看这篇就够了
引言 在使用asp.net core 进行api开发完成后,书写api说明文档对于程序员来说想必是件很痛苦的事情吧,但文档又必须写,而且文档的格式如果没有具体要求的话,最终完成的文档则完全取决于开发者 ...
- ASP.NET Core WebApi使用Swagger生成api
引言 在使用asp.net core 进行api开发完成后,书写api说明文档对于程序员来说想必是件很痛苦的事情吧,但文档又必须写,而且文档的格式如果没有具体要求的话,最终完成的文档则完全取决于开发者 ...
- ASP.NET Core WebApi使用Swagger生成api说明文档
1. Swagger是什么? Swagger 是一个规范和完整的框架,用于生成.描述.调用和可视化 RESTful 风格的 Web 服务.总体目标是使客户端和文件系统作为服务器以同样的速度来更新.文件 ...
- 【转】ASP.NET Core WebApi使用Swagger生成api说明文档看这篇就够了
原文链接:https://www.cnblogs.com/yilezhu/p/9241261.html 引言 在使用asp.net core 进行api开发完成后,书写api说明文档对于程序员来说想必 ...
- Asp.net Core WebApi 使用Swagger做帮助文档,并且自定义Swagger的UI
WebApi写好之后,在线帮助文档以及能够在线调试的工具是专业化的表现,而Swagger毫无疑问是做Docs的最佳工具,自动生成每个Controller的接口说明,自动将参数解析成json,并且能够在 ...
- Asp.net core WebApi 使用Swagger生成帮助页
最近我们团队一直进行.net core的转型,web开发向着前后端分离的技术架构演进,我们后台主要是采用了asp.net core webapi来进行开发,开始每次调试以及与前端人员的沟通上都存在这效 ...
- Asp.net core WebApi 使用Swagger生成帮助页实例
最近我们团队一直进行.net core的转型,web开发向着前后端分离的技术架构演进,我们后台主要是采用了asp.net core webapi来进行开发,开始每次调试以及与前端人员的沟通上都存在这效 ...
- ASP.NET Core WebApi使用Swagger生成API说明文档【xml注释版】
⒈新建ASP.NET Core WebAPi项目 ⒉添加 NuGet 包 Install-Package Swashbuckle.AspNetCore ⒊Startup中配置 using System ...
- ASP.NET Core WebApi使用Swagger生成API说明文档【特性版】
⒈新建ASP.NET Core WebAPi项目 ⒉添加 NuGet 包 Install-Package Swashbuckle.AspNetCore ⒊Startup中配置 using System ...
随机推荐
- Windows(受控主机)上配置
Powershell版本要求及配置 windows需要使用Powershell4.0及以上版本,入下图所示,如果不是4.0及以上的需要升级 一.升级Powershell至3.0+ 1. 下载并安装Mi ...
- 攻防世界(一)baby_web
攻防世界系列:baby_web 方法一: 按照提示,初始界面自然想到index.php,访问后界面(注意到URL)仍是1.php 打开hackbar查看响应,发现确实有index.php点开看到了Fl ...
- Centos7.4永久修改系统时间
[root@V3B01-zsy yum.repos.d]# date -s "2019-09-24 17:02:30" 2019年 09月 24日 星期二 17:02:30 CST ...
- 10.21 nmap:网络探测工具和安全/端口扫描器
nmap命令 是一款开放源代码的网络探测和安全审核工具,是Network Mapper的缩写.其设计目标是快速地扫描大型网络.nmap可以发现网络上有哪些主机,主机提供了什么服务(应用程序名称和版本号 ...
- unity中UI坐标转3d世界坐标
方法: public static Vector3 UIScreenToWorldPoint(Vector3 uiPostion) { uiPostion = UICamera.mainCamera. ...
- thinkphp api接口 统一结果返回处理类
20210602 修正 wqy的笔记:http://www.upwqy.com/details/216.html 返回结果处理,归根结底 主要是有两点 数据结构和返回的数据类型 1.数据类型 :一般情 ...
- linux 安装配置NFS服务器
一.Ubuntu安装配置NFS 1.安装NFS服务器 sudo apt-get install nfs-kernel-server 安装nfs-kernel-server时,apt会自动安装nfs-c ...
- GO学习-(22) Go语言之依赖管理
Go语言之依赖管理 Go语言的依赖管理随着版本的更迭正逐渐完善起来. 依赖管理 为什么需要依赖管理 最早的时候,Go所依赖的所有的第三方库都放在GOPATH这个目录下面.这就导致了同一个库只能保存一个 ...
- RedisPool
class RedisPool(object): instance = None def __init__(self, host='192.168.2.129', port=7000, passwor ...
- Non-Maximum Suppression,NMS非极大值抑制
Non-Maximum Suppression,NMS非极大值抑制概述非极大值抑制(Non-Maximum Suppression,NMS),顾名思义就是抑制不是极大值的元素,可以理解为局部最大搜索. ...