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

8
我在解决方案中设置了以下控制器:
[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);
    }
}  

在我的 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

如上所述,路径不同是因为传递到路由中的版本参数导致了这种差异。此外,纯粹为了表示代码可以通过文档获得而创建全新方法是没有意义的,因此我的问题是为什么Swagger会忽略路径中的版本差异,并建议使用ConflictingActionsResolver?
此外,进一步调查后发现很多其他人也遇到了相同的问题(特别是头部版本控制的问题),而Swagger的严格做法与此相冲突。通常的解决方法似乎是使用冲突动作解析器(conflicting actions resolver)来仅获取遇到的第一个描述,这样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/OpenAPISpecification{description.GroupName}/swagger.json",
                            description.GroupName.ToUpperInvariant());
   }
});


如何解决这个问题,并在不创建新方法的情况下正确显示Swagger中可用的端点?这似乎是Swagger规范的一个疏漏,我们应该如何做?非常感谢您的帮助。
注意:许多人可能建议在路由末尾附加操作,但我们希望避免这种情况,因为这意味着我们的端点不符合RESTful规范。我们希望像customers/1这样的格式,使用GET、POST、PUT属性派生CRUD操作,而不必追加类似于customers/add_customer_1或customers/add_customer_2的内容反映URL中的方法名称。

他们把我们的对话转移到了聊天中,所以也许你没有看到我的消息,我在那里写道我的公共存储库已经更新了你的代码。经过一些修改,事情正在运作。问候。 - Roar S.
2个回答

3
这是我使用 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; }
    }
}

在 Startup#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 
});           

在 Startup#Configure 中

    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"
      }
    ]
  }
}

评论不是用于延长讨论的;此对话已被移至聊天室 - Samuel Liew

0

有几个问题。

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

因此:

[Route("api/v{VersionId}/[controller]")]

应该是:

[Route("api/v{VersionId:apiVersion}/[controller]")]

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

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

确保您的API Explorer配置具有:

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

这将生成路径,将api/v{VersionId:apiVersion}/[controller]扩展为分别为api/v1/Authenticationapi/v1.1/Authentication


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接