【问题标题】:Custom operation IDs for single document, and not the other单个文档的自定义操作 ID,而不是其他文档
【发布时间】:2021-06-23 13:43:45
【问题描述】:

我有两个 Swagger 文档:

options.SwaggerDoc("v1", new OpenApiInfo
{
    Title = "V1",
    Version = "v1"
});
options.SwaggerDoc("v2", new OpenApiInfo
{
    Title = "V2",
    Version = "v2"
});

我想对其中一个应用CustomOperationIds,对另一个应用另一种类型,以便V2的操作ID与V1不同。

这可能吗?

【问题讨论】:

  • 您是否使用Microsoft.AspNetCore.Mvc.Versioning 进行版本控制?或者您如何区分相同 API 的版本?

标签: c# swashbuckle swashbuckle.aspnetcore


【解决方案1】:

是的,这是可能的。正如您所发现的,Swashbuckle 提供了SwaggerGenOptions.CustomOperationIds() 扩展点。我们可以参与其中。

您需要有一个控制器,其动作用[ApiExplorerSettings] 属性注释。这可确保操作最终出现在正确的 OpenAPI 文档中。

这里我使用Microsoft.AspNetCore.Mvc.Versioning 库进行 API 版本控制。这样我就可以对不同版本对应的不同操作使用相同的路径。但这不是必需的。

[ApiController]
[Route("api/[controller]")]
public class StuffController: ControllerBase
{
    [HttpGet("")]
    [ApiVersion("1.0")]
    [ApiExplorerSettings(GroupName = "v1")]
    public IActionResult GetStuffTheOldWay()
    {
        return Ok(nameof(GetStuffTheOldWay));
    }
    
    [HttpGet("")]
    [ApiVersion("2.0")]
    [ApiExplorerSettings(GroupName = "v2")]
    public IActionResult GetStuffTheNewWay()
    {
        return Ok(nameof(GetStuffTheNewWay));
    }
}

然后我们可以在构建操作 ID 时使用该组名。

services.AddSwaggerGen(
    c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "ApiPlayground", Version = "v1" });
        c.SwaggerDoc("v2", new OpenApiInfo { Title = "ApiPlayground", Version = "v2" });
        c.CustomOperationIds(
            description =>
            {
                if (!(description.ActionDescriptor is ControllerActionDescriptor actionDescriptor))
                {
                    return null; // default behavior
                }

                return description.GroupName switch
                {
                    "v1" => $"Old{actionDescriptor.ActionName}",
                    "v2" => $"New{actionDescriptor.ActionName}",
                    _ => null // default behavior
                };
            });
    }
);

这为我们提供了两个 OpenAPI 文档,它们根据 [ApiVersionAttribute] 组正确地为操作 ID 添加前缀。

// v1 API:

{
  "openapi": "3.0.1",
  "info": {
    "title": "ApiPlayground",
    "version": "v1"
  },
  "paths": {
    "/api/Stuff": {
      "get": {
        "tags": [
          "Stuff"
        ],
        "operationId": "OldGetStuffTheOldWay", // <---
        // ...
}

// v2 API:
{
  "openapi": "3.0.1",
  "info": {
    "title": "ApiPlayground",
    "version": "v2"
  },
  "paths": {
    "/api/Stuff": {
      "get": {
        "tags": [
          "Stuff"
        ],
        "operationId": "NewGetStuffTheNewWay", // <---
        // ...
}

一旦您拥有actionDescriptor,您就可以访问 ASP.NET Core 提供的大量元数据供您使用:

在 Swagger UI 中根据版本设置默认参数

Swagger UI 很不错,但它只执行默认 API 版本的操作。我们需要将 API 版本指定为查询参数 api-version=1.0 或在标头中,或作为 URL 的一部分。 为了表达这个要求,我们可以稍微修改一下 OpenAPI 文档并添加一个版本参数,该参数默认为端点对应的任何版本。那就是:

  • v1 -> 必须有?api-version=1.0 参数
  • v2 -> 必须有?api-version=2.0 参数

等等。 Swashbuckle 还有另一个扩展点,operation filters

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddSwaggerGen(
        c =>
        {
            c.SwaggerDoc("1.0", new OpenApiInfo { Title = "ApiPlayground", Version = "v1" });
            c.SwaggerDoc("2.0", new OpenApiInfo { Title = "ApiPlayground", Version = "v2" });
            c.CustomOperationIds(...);
            c.OperationFilter<ApiVersionFilter>(); // <---
        }
    );
}


private class ApiVersionFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        // find all defined versions
        var versions = context.ApiDescription
            .ActionDescriptor
            .EndpointMetadata
            .OfType<ApiVersionAttribute>()
            .SelectMany(a => a.Versions)
            .Select(v => v.ToString()).ToList();
        
        if (!versions.Any())
        {
            return;
        }
        
        // extend openapi schema with a version selector
        var firstVersion = versions.First();
        var versionEnum = versions.Select(v => new OpenApiString(v)).Cast<IOpenApiAny>().ToList();
        operation.Parameters.Add(
            new OpenApiParameter
            {
                In = ParameterLocation.Query,
                Name = "api-version",
                Description = "The version of the API you want to call",
                Example = new OpenApiString(firstVersion),
                Schema = new OpenApiSchema
                {
                    Type = "string",
                    Enum = versionEnum
                }
            }
        );
    }
}

完成后,我们会得到一个填充了 api-version 值的 UI。

【讨论】:

  • 如果我不想为我的所有控制器操作添加注释,而只想为我的整个项目添加 2 个 swagger 文件,并使用两种不同的设置,该怎么办?
  • 您不必对每个操作进行注释,您可以在操作过滤器中分配GroupNames。你能更详细地解释一下你的用例吗?
  • 我正在尝试制作一个具有自定义操作 ID 的 Swagger 文件,遵循一种命名约定,但同时也制作另一个具有不同命名约定的 Swagger 文件。两个 Swagger 文件都包含相同的操作,但命名约定不同。希望这是有道理的。
  • 哦,我明白了,所以您只想克隆一个文档,然后更改其操作 ID?
  • 好的,完成了。我正在准备第二个答案,因为它不适合这个
【解决方案2】:

提供具有不同操作 ID 的现有 Swagger 文档

这有点牵强。 Swashbuckle 让我们一次过滤单个文档。我们应该能够在文档过滤器中注入ISwaggerProvider,这会大大简化事情,但我无法让它工作。尽管如此,我们可以装饰现有的ISwaggerProvider 实现并提供修改后的文档。

第一步:设置操作ID

默认情况下,操作没有操作 ID,因此请使用 SwaggerGenOptions.CustomOperationIds() 填充值。

services.AddSwaggerGen(
    c =>
    {
        c.SwaggerDoc("v1", new OpenApiInfo { Title = "ApiPlayground", Version = "v1" });
        // use action names as operations ids if present
        c.CustomOperationIds(
            description => description.ActionDescriptor is not ControllerActionDescriptor actionDescriptor
                ? null
                : actionDescriptor.ActionName);
    }
);

第 2 步:为 Swagger UI 定义两个文档

这会让 Swagger UI 在 doc 下拉菜单中显示两个不同的文档。这里我定义了两个文档,一个带有ops 后缀,一个没有。

// inside Startup class
public void Configure(IApplicationBuilder app)
{
    app.UseSwagger();
    app.UseSwaggerUI(c =>
    {
        c.SwaggerEndpoint("/swagger/v1/swagger.json", "ApiPlayground v1");
        c.SwaggerEndpoint("/swagger/v1ops/swagger.json", "ApiPlayground v1ops");
        c.DisplayOperationId();
    });
    // ...
}

它的外观如下:

我们将拦截v1ops 的请求并提供修改后的文档。

第 3 步:实现自定义 ISwaggerProvider

这是提供自定义 OpenAPI 文档的必要条件,同时仍能够访问现有文档以获取实际值。一旦我们得到原始文档,我们就修改每个端点的操作 id。

所以,我编写了一个代理,它继承了SwaggerGenerator 并同时实现了ISwaggerProvider。这可确保the method we've hidden with new keyword actually gets called.

这里,这个类拦截文档请求并检查文档名称是否以ops为后缀,然后提供带有修改操作ID的原始文档。

class SwaggerDocCustomOperationIdProvider : SwaggerGenerator, ISwaggerProvider
{
    public SwaggerDocCustomOperationIdProvider(SwaggerGeneratorOptions options, IApiDescriptionGroupCollectionProvider apiDescriptionsProvider, ISchemaGenerator schemaGenerator) : base(options, apiDescriptionsProvider, schemaGenerator)
    {
    }

    public new OpenApiDocument GetSwagger(string documentName, string host = null, string basePath = null)
    {
        if (!documentName.EndsWith("ops"))
        {
            return base.GetSwagger(documentName, host, basePath);
        }

        var sourceDoc = documentName.Replace("ops", "");
        var doc = base.GetSwagger(sourceDoc, host, basePath);

        // dont mutate the info props, because Swashbuckle caches the docs
        doc.Info = new OpenApiInfo
        {
            Title = $"{doc.Info.Title} - with operation ids",
            Version = doc.Info.Version,
        };

        var operations = doc.Paths
            .SelectMany(p => p.Value.Operations.Values)
            .ToList();

        foreach (var op in operations)
        {
            // change the operation id
            op.OperationId = $"Cloned{op.OperationId}";
        }

        return doc;
    }
}

然后,我们将其注册到 DI 容器中。

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddSwaggerGen(/* ... */);
    // get swashbuckle to use our implementation
    services.AddTransient<ISwaggerProvider, SwaggerDocCustomOperationIdProvider>();
}

结果

原始文件:

具有自定义操作 ID 的一个:

注意事项:

由于我们正在修改最终文档,因此我们无法像原始 SwaggerGenerator 那样访问反射数据。这意味着您不能轻易引用运行时方法信息。但您始终可以实现 IDocumentFilter 并从头开始准备架构。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-11
    • 1970-01-01
    • 2019-06-15
    • 1970-01-01
    • 2020-12-08
    相关资源
    最近更新 更多