操作的冲突方法/路径组合 - Swagger 无法将替代版本与 Route 区分开来

Posted

技术标签:

【中文标题】操作的冲突方法/路径组合 - Swagger 无法将替代版本与 Route 区分开来【英文标题】:Conflicting method/path combination for action - Swagger unable to distinguish alternate version from Route 【发布时间】:2020-12-21 14:36:06 【问题描述】:

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

[Route("api/vVersionId/[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);
    
  

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

运行此代码时,自然一切都会正常运行。该代码是有效的,并且 .net 核心允许这种实现,但是,当涉及到 swagger gen 时,我遇到了它产生以下错误的问题:

NotSupportedException: Conflicting method/path combination "POST api/vVersionId/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

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

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

Swagger UI Config

app.UseSwaggerUI(setup =>

   setup.RoutePrefix = string.Empty;

   foreach (var description in apiVersions.ApiVersionDescriptions)
   
      setup.SwaggerEndpoint($"/swagger/OpenAPISpecificationdescription.GroupName/swagger.json",
                            description.GroupName.ToUpperInvariant());
   
);

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

注意许多人可能会建议将操作附加到路由的末尾,但是我们希望避免这种情况,因为这意味着我们的端点并不安宁,因为我们想要使用派生 CRUD 操作的 GET、POST、PUT 属性来争取像 customers/1 这样的东西无需附加诸如customers/add_customer_1 或customers/add_customer_2 之类的内容以反映URL 中的方法名称。

【问题讨论】:

他们将我们的对话转移到聊天中,因此您可能没有看到我写的我的公共存储库已使用您的代码更新的消息。经过一些修改后一切正常。 BR 【参考方案1】:

这是我在使用HeaderApiVersionReader 时的 Swagger 设置。

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; 
    

在启动#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 
);           

在启动#配置中

    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");
      
    );

appsettings.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"
      
    ]
  

【讨论】:

评论不用于扩展讨论;这个对话是moved to chat。【参考方案2】:

有几个问题。

第一个问题是路由模板确实包含路由约束。在按 URL 段进行版本控制时,这是必需的。

因此:

[Route("api/vVersionId/[controller]")]

应该是:

[Route("api/vVersionId:apiVersion/[controller]")]

许多示例将显示使用version 作为路由参数名称,但您可以使用VersionId 或您想要的任何其他名称。

第二个问题是您可能正在创建单个 OpenAPI/Swagger 文档。该文件要求每个路由模板都是唯一的。 Swashbuckle 中的默认行为是每个 API 版本的文档。此方法将生成唯一路径。如果你真的想要一个文档,可以使用 URL 段版本控制,但你需要扩展路由模板,以便它们产生唯一的路径。

确保您的 API Explorer 配置具有:

services.AddVersionedApiExplorer(options => options.SubstituteApiVersionInUrl = true);

这将生成将api/vVersionId:apiVersion/[controller] 分别扩展为api/v1/Authenticationapi/v1.1/Authentication 的路径。

【讨论】:

以上是关于操作的冲突方法/路径组合 - Swagger 无法将替代版本与 Route 区分开来的主要内容,如果未能解决你的问题,请参考以下文章

解决springboot2.6和swagger冲突的四种方法

解决springboot2.6和swagger冲突的问题

实战篇:解决swagger和自定义参数解析器的功能冲突

Swagger 3.0切换requestBody的类型,按路径

Swagger 2.0不支持:带路径的多个操作

Spring Boot 自定义 Swagger2 请求 URL 路径的两种方法