[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所以找到另一個途徑解決問題。

文章轉自微信公眾號@DotNET技術圈

上一篇:

優化REST API資源跨域請求:啟用CORS的簡明步驟

下一篇:

Django REST framework實現API之基礎篇
#你可能也喜歡這些API文章!

我們有何不同?

API服務商零注冊

多API并行試用

數據驅動選型,提升決策效率

查看全部API→
??

熱門場景實測,選對API

#AI文本生成大模型API

對比大模型API的內容創意新穎性、情感共鳴力、商業轉化潛力

25個渠道
一鍵對比試用API 限時免費

#AI深度推理大模型API

對比大模型API的邏輯推理準確性、分析深度、可視化建議合理性

10個渠道
一鍵對比試用API 限時免費