缘起

在使用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其实就是咱们常用HttpGetHttpPostHttpPutHttpDelete相关的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报错引发的探究的更多相关文章

  1. ASP.NET Core WebApi使用Swagger生成api说明文档看这篇就够了

    引言 在使用asp.net core 进行api开发完成后,书写api说明文档对于程序员来说想必是件很痛苦的事情吧,但文档又必须写,而且文档的格式如果没有具体要求的话,最终完成的文档则完全取决于开发者 ...

  2. ASP.NET Core WebApi使用Swagger生成api

    引言 在使用asp.net core 进行api开发完成后,书写api说明文档对于程序员来说想必是件很痛苦的事情吧,但文档又必须写,而且文档的格式如果没有具体要求的话,最终完成的文档则完全取决于开发者 ...

  3. ASP.NET Core WebApi使用Swagger生成api说明文档

    1. Swagger是什么? Swagger 是一个规范和完整的框架,用于生成.描述.调用和可视化 RESTful 风格的 Web 服务.总体目标是使客户端和文件系统作为服务器以同样的速度来更新.文件 ...

  4. 【转】ASP.NET Core WebApi使用Swagger生成api说明文档看这篇就够了

    原文链接:https://www.cnblogs.com/yilezhu/p/9241261.html 引言 在使用asp.net core 进行api开发完成后,书写api说明文档对于程序员来说想必 ...

  5. Asp.net Core WebApi 使用Swagger做帮助文档,并且自定义Swagger的UI

    WebApi写好之后,在线帮助文档以及能够在线调试的工具是专业化的表现,而Swagger毫无疑问是做Docs的最佳工具,自动生成每个Controller的接口说明,自动将参数解析成json,并且能够在 ...

  6. Asp.net core WebApi 使用Swagger生成帮助页

    最近我们团队一直进行.net core的转型,web开发向着前后端分离的技术架构演进,我们后台主要是采用了asp.net core webapi来进行开发,开始每次调试以及与前端人员的沟通上都存在这效 ...

  7. Asp.net core WebApi 使用Swagger生成帮助页实例

    最近我们团队一直进行.net core的转型,web开发向着前后端分离的技术架构演进,我们后台主要是采用了asp.net core webapi来进行开发,开始每次调试以及与前端人员的沟通上都存在这效 ...

  8. ASP.NET Core WebApi使用Swagger生成API说明文档【xml注释版】

    ⒈新建ASP.NET Core WebAPi项目 ⒉添加 NuGet 包 Install-Package Swashbuckle.AspNetCore ⒊Startup中配置 using System ...

  9. ASP.NET Core WebApi使用Swagger生成API说明文档【特性版】

    ⒈新建ASP.NET Core WebAPi项目 ⒉添加 NuGet 包 Install-Package Swashbuckle.AspNetCore ⒊Startup中配置 using System ...

随机推荐

  1. Hive 安装和配置

    环境准备 已安装 HDFS 和 Yarn 集群环境 Windows 已安装 MySQL 8 (开启远程连接用户权限) 安装步骤 1. 上传并解压 Hive 安装文件 将 apache-hive-3.1 ...

  2. zabbix监控之概念和安装

    一.为什么要要监控 (1)在需要的时刻,提前提醒我们服务器出问题了: (2)当出问题之后,可以找到问题的根源: (3)检查网站/服务器的可用性 1.监控范畴 硬件监控.系统监控.服务监控.性能监控.日 ...

  3. Java forEach 方式遍历集合(Java 8 新特性)

    JDK 8 新增 forEach 方式遍历集合,这种方式比原来的 for each 循环还要简洁和便利. 需要注意:如果你计算机安装的是 JDK 8 以前的版本,是不支持 JDK 8 的新特性 Lis ...

  4. Java中生成唯一标识符的方法

    有时候业务需要生成唯一标识符,但又不能依赖于数据库中自动递增的字段产生唯一ID,比如多表同一字段需要统一一个唯一ID,此时我们就需要用程序来生成一个唯一的全局ID. UUID UUID是指在一台机器上 ...

  5. 本地软件仓库配置及NFS安装

    [root@localhost ~]# mount /dev/cdrom /mnt mount: /dev/sr0 is write-protected, mounting read-only [ro ...

  6. Jenkins代码自动部署相关文档

    环境 centos 7.0+ Java JDK 1.8+ jenkins 2.220 maven 3.0+ git 1.8+ 注意事项 一. linux 安装 JDK (jdk-8u201-linux ...

  7. HashMap源码:聊聊Map的遍历性能问题(一)

    目录 引言 迭代器测试 迭代器源码探究 其他遍历方法 增强型for循环 Map.forEach Stream.forEach 总结 附:四种遍历源码 附:完整测试类与测试结果+一个奇怪的问题 引言 今 ...

  8. 孟老板 ListAdapter封装, 告别Adapter代码 (上)

    BaseAdapter封装(一) 简单封装 BaseAdapter封装(二) Header,footer BaseAdapter封装(三) 空数据占位图 BaseAdapter封装(四) PageHe ...

  9. 深入理解java虚拟机笔记Chapter7

    虚拟机类的加载机制 概述 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类的加载机制. 类加载的时机 J ...

  10. jsp页面抽取

    步骤: 1.先将jsp中要抽取的公共部分剪切出来,黏贴到新的jsp文件中,取名叫xxx.jsp 2.在需要引入此公共部分的jsp页面中使用<%@include file="xxx.js ...