操作的方法/路径组合冲突 - Swagger 无法区分替代版本和路线

jez*_*pin 8 swagger swagger-ui api-versioning asp.net-core

我的解决方案中有以下控制器设置:

[Route("api/v{VersionId}/[controller]")]
[ApiController]
[Produces("application/json")]
[Consumes("application/json")]
public class MyBaseController : ControllerBase
{
}

[ApiVersion("1.0")]
[ApiVersion("1.1")]
public class AuthenticationController : MyBaseController
{
    private readonly ILoginService _loginService;

    public AuthenticationController(ILoginService loginService)
    {
        _loginService = loginService;
    }

    [ProducesResponseType(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status403Forbidden)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [HttpPost("login")]
    public ActionResult<v1.JwtTokenResponse> Login([FromBody] v1.LoginRequest loginRequest)
    {
        var loginResult = _loginService.Login(loginRequest.Email, loginRequest.Password);
        if (loginResult.StatusCode != HttpStatusCode.OK)
        {
            return StatusCode((int)loginResult.StatusCode);
        }

        var tokenResponse = new v1.JwtTokenResponse() { Token = loginResult.Token };

        return Ok(tokenResponse);
    }
}  
Run Code Online (Sandbox Code Playgroud)

在我的 API 的两个版本之间,此方法没有任何变化,因此从逻辑上讲,我想在我的文档中显示新版本仍然支持该方法。假设我们有第二个客户控制器,它的逻辑发生了一些变化,因此这就是我们拥有新版本 1.1 的原因,因为语义版本控制指示已添加新内容,但以向后兼容的方式添加。

运行这段代码时,自然一切都会构建得很好。代码是有效的,.net core 允许这种实现,但是,当涉及到 swagger gen 时,我遇到了问题,并产生以下错误:

NotSupportedException: Conflicting method/path combination "POST api/v{VersionId}/Authentication/login" for actions - Template.Api.Endpoints.Controllers.AuthenticationController.Login (Template.Api.Endpoints),Template.Api.Endpoints.Controllers.AuthenticationController.Login (Template.Api.Endpoints). Actions require a unique method/path combination for Swagger/OpenAPI 3.0. Use ConflictingActionsResolver as a workaround
Run Code Online (Sandbox Code Playgroud)

正如您在上面所看到的,路径是不同的,因为传递到路由中的版本参数使其如此。此外,纯粹创建一个全新的方法来表示代码可通过文档获得是没有意义的,所以我的问题是为什么 swagger 会忽略路径中的版本差异并建议 ConflictingActionsResolver 的用户?

此外,在进一步深入研究并发现很多其他人也遇到同样的问题之后(标头版本控制是社区的一个特定问题,而 Swaggers 的强硬方法与此相冲突),一般方法似乎是使用冲突的操作解析器仅获取它遇到的第一个描述,这只会在 api 文档中公开版本 1.0,而忽略 1.1 版本,从而在 Swagger 中给人留下没有可用端点的 1.1 版本的印象。

NotSupportedException: Conflicting method/path combination "POST api/v{VersionId}/Authentication/login" for actions - Template.Api.Endpoints.Controllers.AuthenticationController.Login (Template.Api.Endpoints),Template.Api.Endpoints.Controllers.AuthenticationController.Login (Template.Api.Endpoints). Actions require a unique method/path combination for Swagger/OpenAPI 3.0. Use ConflictingActionsResolver as a workaround
Run Code Online (Sandbox Code Playgroud)

我们如何解决这个问题并在 Swagger 中正确显示可用端点,而不必创建新方法来有效地导致代码重复,只是为了满足 Swagger 规范中看似疏忽的情况?任何帮助将不胜感激。

注意:许多人可能建议在路由末尾附加操作,但我们希望避免这种情况,因为这意味着我们的端点在我们想要争取诸如具有派生 CRUD 的 GET、POST、PUT 属性的客户/1 之类的内容时不稳定操作,而无需附加诸如customers/add_customer_1或customers/add_customer_2之类的内容来反映URL中的方法名称。

Roa*_* S. 3

这是我使用时的 Swagger 设置HeaderApiVersionReader

public class SwaggerOptions
{
    public string Title { get; set; }
    public string JsonRoute { get; set; }
    public string Description { get; set; }
    public List<Version> Versions { get; set; }

    public class Version
    {
        public string Name { get; set; }
        public string UiEndpoint { get; set; }
    }
}
Run Code Online (Sandbox Code Playgroud)

在启动#ConfigureServices中

services.AddApiVersioning(apiVersioningOptions =>
{
    apiVersioningOptions.AssumeDefaultVersionWhenUnspecified = true;
    apiVersioningOptions.DefaultApiVersion = new ApiVersion(1, 0);
    apiVersioningOptions.ReportApiVersions = true;
    apiVersioningOptions.ApiVersionReader = new HeaderApiVersionReader("api-version");
});

// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(swaggerGenOptions =>
{
    var swaggerOptions = new SwaggerOptions();
    Configuration.GetSection("Swagger").Bind(swaggerOptions);

    foreach (var currentVersion in swaggerOptions.Versions)
    {
        swaggerGenOptions.SwaggerDoc(currentVersion.Name, new OpenApiInfo
        {
            Title = swaggerOptions.Title,
            Version = currentVersion.Name,
            Description = swaggerOptions.Description
        });
    }

    swaggerGenOptions.DocInclusionPredicate((version, desc) =>
    {
        if (!desc.TryGetMethodInfo(out MethodInfo methodInfo))
        {
            return false;
        }
        var versions = methodInfo.DeclaringType.GetConstructors()
            .SelectMany(constructorInfo => constructorInfo.DeclaringType.CustomAttributes
                .Where(attributeData => attributeData.AttributeType == typeof(ApiVersionAttribute))
                .SelectMany(attributeData => attributeData.ConstructorArguments
                    .Select(attributeTypedArgument => attributeTypedArgument.Value)));

        return versions.Any(v => $"{v}" == version);
    });

    swaggerGenOptions.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"));
    
    //... some filter settings here 
});           
Run Code Online (Sandbox Code Playgroud)

在启动#配置中

    var swaggerOptions = new SwaggerOptions();
    Configuration.GetSection("Swagger").Bind(swaggerOptions);
    app.UseSwagger(option => option.RouteTemplate = swaggerOptions.JsonRoute);

    app.UseSwaggerUI(option =>
    {
      foreach (var currentVersion in swaggerOptions.Versions)
      {
        option.SwaggerEndpoint(currentVersion.UiEndpoint, $"{swaggerOptions.Title} {currentVersion.Name}");
      }
    });
Run Code Online (Sandbox Code Playgroud)

应用程序设置.json

{
  "Swagger": {
    "Title": "App title",
    "JsonRoute": "swagger/{documentName}/swagger.json",
    "Description": "Some text",
    "Versions": [
      {
        "Name": "2.0",
          "UiEndpoint": "/swagger/2.0/swagger.json"
      },
      {
        "Name": "1.0",
        "UiEndpoint": "/swagger/1.0/swagger.json"
      }
    ]
  }
}
Run Code Online (Sandbox Code Playgroud)