mirror of
https://github.com/bitwarden/server
synced 2025-12-25 12:43:14 +00:00
[PM-25182] Improve swagger OperationIDs: Part 1 (#6229)
* Improve swagger OperationIDs: Part 1 * Fix tests and fmt * Improve docs and add more tests * Fmt * Improve Swagger OperationIDs for Auth * Fix review feedback * Use generic getcustomattributes * Format * replace swaggerexclude by split+obsolete * Format * Some remaining excludes
This commit is contained in:
25
src/SharedWeb/Swagger/ActionNameOperationFilter.cs
Normal file
25
src/SharedWeb/Swagger/ActionNameOperationFilter.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System.Text.Json;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Bit.SharedWeb.Swagger;
|
||||
|
||||
/// <summary>
|
||||
/// Adds the action name (function name) as an extension to each operation in the Swagger document.
|
||||
/// This can be useful for the code generation process, to generate more meaningful names for operations.
|
||||
/// Note that we add both the original action name and a snake_case version, as the codegen templates
|
||||
/// cannot do case conversions.
|
||||
/// </summary>
|
||||
public class ActionNameOperationFilter : IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (!context.ApiDescription.ActionDescriptor.RouteValues.TryGetValue("action", out var action)) return;
|
||||
if (string.IsNullOrEmpty(action)) return;
|
||||
|
||||
operation.Extensions.Add("x-action-name", new OpenApiString(action));
|
||||
// We can't do case changes in the codegen templates, so we also add the snake_case version of the action name
|
||||
operation.Extensions.Add("x-action-name-snake-case", new OpenApiString(JsonNamingPolicy.SnakeCaseLower.ConvertName(action)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Bit.SharedWeb.Swagger;
|
||||
|
||||
/// <summary>
|
||||
/// Checks for duplicate operation IDs in the Swagger document, and throws an error if any are found.
|
||||
/// Operation IDs must be unique across the entire Swagger document according to the OpenAPI specification,
|
||||
/// but we use controller action names to generate them, which can lead to duplicates if a Controller function
|
||||
/// has multiple HTTP methods or if a Controller has overloaded functions.
|
||||
/// </summary>
|
||||
public class CheckDuplicateOperationIdsDocumentFilter(bool printDuplicates = true) : IDocumentFilter
|
||||
{
|
||||
public bool PrintDuplicates { get; } = printDuplicates;
|
||||
|
||||
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
|
||||
{
|
||||
var operationIdMap = new Dictionary<string, List<(string Path, OpenApiPathItem PathItem, OperationType Method, OpenApiOperation Operation)>>();
|
||||
|
||||
foreach (var (path, pathItem) in swaggerDoc.Paths)
|
||||
{
|
||||
foreach (var operation in pathItem.Operations)
|
||||
{
|
||||
if (!operationIdMap.TryGetValue(operation.Value.OperationId, out var list))
|
||||
{
|
||||
list = [];
|
||||
operationIdMap[operation.Value.OperationId] = list;
|
||||
}
|
||||
|
||||
list.Add((path, pathItem, operation.Key, operation.Value));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Find duplicates
|
||||
var duplicates = operationIdMap.Where((kvp) => kvp.Value.Count > 1).ToList();
|
||||
if (duplicates.Count > 0)
|
||||
{
|
||||
if (PrintDuplicates)
|
||||
{
|
||||
Console.WriteLine($"\n######## Duplicate operationIds found in the schema ({duplicates.Count} found) ########\n");
|
||||
|
||||
Console.WriteLine("## Common causes of duplicate operation IDs:");
|
||||
Console.WriteLine("- Multiple HTTP methods (GET, POST, etc.) on the same controller function");
|
||||
Console.WriteLine(" Solution: Split the methods into separate functions, and if appropiate, mark the deprecated ones with [Obsolete]");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("- Overloaded controller functions with the same name");
|
||||
Console.WriteLine(" Solution: Rename the overloaded functions to have unique names, or combine them into a single function with optional parameters");
|
||||
Console.WriteLine();
|
||||
|
||||
Console.WriteLine("## The duplicate operation IDs are:");
|
||||
|
||||
foreach (var (operationId, duplicate) in duplicates)
|
||||
{
|
||||
Console.WriteLine($"- operationId: {operationId}");
|
||||
foreach (var (path, pathItem, method, operation) in duplicate)
|
||||
{
|
||||
Console.Write($" {method.ToString().ToUpper()} {path}");
|
||||
|
||||
|
||||
if (operation.Extensions.TryGetValue("x-source-file", out var sourceFile) && operation.Extensions.TryGetValue("x-source-line", out var sourceLine))
|
||||
{
|
||||
var sourceFileString = ((Microsoft.OpenApi.Any.OpenApiString)sourceFile).Value;
|
||||
var sourceLineString = ((Microsoft.OpenApi.Any.OpenApiInteger)sourceLine).Value;
|
||||
|
||||
Console.WriteLine($" {sourceFileString}:{sourceLineString}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine();
|
||||
}
|
||||
}
|
||||
Console.WriteLine("\n");
|
||||
}
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Duplicate operation IDs found in Swagger schema");
|
||||
}
|
||||
}
|
||||
}
|
||||
33
src/SharedWeb/Swagger/SwaggerGenOptionsExt.cs
Normal file
33
src/SharedWeb/Swagger/SwaggerGenOptionsExt.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
|
||||
namespace Bit.SharedWeb.Swagger;
|
||||
|
||||
public static class SwaggerGenOptionsExt
|
||||
{
|
||||
|
||||
public static void InitializeSwaggerFilters(
|
||||
this SwaggerGenOptions config, IWebHostEnvironment environment)
|
||||
{
|
||||
config.SchemaFilter<EnumSchemaFilter>();
|
||||
config.SchemaFilter<EncryptedStringSchemaFilter>();
|
||||
|
||||
config.OperationFilter<ActionNameOperationFilter>();
|
||||
|
||||
// Set the operation ID to the name of the controller followed by the name of the function.
|
||||
// Note that the "Controller" suffix for the controllers, and the "Async" suffix for the actions
|
||||
// are removed already, so we don't need to do that ourselves.
|
||||
// TODO(Dani): This is disabled until we remove all the duplicate operation IDs.
|
||||
// config.CustomOperationIds(e => $"{e.ActionDescriptor.RouteValues["controller"]}_{e.ActionDescriptor.RouteValues["action"]}");
|
||||
// config.DocumentFilter<CheckDuplicateOperationIdsDocumentFilter>();
|
||||
|
||||
// These two filters require debug symbols/git, so only add them in development mode
|
||||
if (environment.IsDevelopment())
|
||||
{
|
||||
config.DocumentFilter<GitCommitDocumentFilter>();
|
||||
config.OperationFilter<SourceFileLineOperationFilter>();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user