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

问题描述

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

[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 核心允许这种实现,但是,当涉及到 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

正如你在上面看到的,路径是不同的,因为传递给路由的版本参数是这样的。此外,仅仅创建一个全新的方法来表示代码可通过文档获得是没有意义的,所以我的问题是为什么大摇大摆地忽略路径中的版本差异并建议用户使用 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/OpenAPISpecification{description.GroupName}/swagger.json",
                            description.GroupName.ToUpperInvariant());
   }
});

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

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

标签: asp.net-coreswaggerswagger-uiapi-versioning

解决方案


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

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

应用设置.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"
      }
    ]
  }
}

推荐阅读