Add DualInputDocumentFilter for Swagger content merging

Introduced the `DualInputDocumentFilter` class to merge Swagger operations with the same path but different `[Consumes]` attributes (`multipart/form-data` and `application/json`) into a single operation. This ensures both content types are visible in the Swagger UI.

Updated `SwaggerConfiguration.cs` to:
- Resolve conflicting actions by keeping the first variant.
- Register the `DualInputDocumentFilter` to enable content type merging.
This commit is contained in:
2026-07-13 16:04:55 +02:00
parent f7433111a7
commit 386a124a4e
2 changed files with 132 additions and 0 deletions

View File

@@ -0,0 +1,125 @@
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace DocumentOperator.API.Configuration
{
/// <summary>
/// Swagger document filter that merges operations with same path but different [Consumes] attributes.
/// Ensures both multipart/form-data and application/json variants are visible in Swagger UI.
/// </summary>
public class DualInputDocumentFilter : IDocumentFilter
{
private readonly IApiDescriptionGroupCollectionProvider _apiDescriptionProvider;
/// <summary>
/// Initializes a new instance of the <see cref="DualInputDocumentFilter"/> class.
/// </summary>
/// <param name="apiDescriptionProvider">API description provider to access all endpoints</param>
public DualInputDocumentFilter(IApiDescriptionGroupCollectionProvider apiDescriptionProvider)
{
_apiDescriptionProvider = apiDescriptionProvider;
}
/// <summary>
/// Applies the filter to merge operations with different content types.
/// </summary>
public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
var allApiDescriptions = _apiDescriptionProvider.ApiDescriptionGroups.Items
.SelectMany(g => g.Items)
.ToList();
// Group by path
var groupedByPath = allApiDescriptions
.GroupBy(x => "/" + x.RelativePath)
.ToList();
foreach (var group in groupedByPath)
{
var path = group.Key;
if (!swaggerDoc.Paths.ContainsKey(path))
continue;
var pathItem = swaggerDoc.Paths[path];
// Find multipart and JSON variants
var multipartDesc = group.FirstOrDefault(x =>
x.SupportedRequestFormats.Any(f => f.MediaType == "multipart/form-data"));
var jsonDesc = group.FirstOrDefault(x =>
x.SupportedRequestFormats.Any(f => f.MediaType == "application/json"));
// If we have both variants, merge them into single operation
if (multipartDesc != null && jsonDesc != null)
{
var httpMethod = multipartDesc.HttpMethod?.ToLowerInvariant();
OperationType operationType;
if (!Enum.TryParse<OperationType>(httpMethod, true, out operationType))
continue;
if (!pathItem.Operations.ContainsKey(operationType))
continue;
var operation = pathItem.Operations[operationType];
// Ensure RequestBody exists
if (operation.RequestBody == null)
{
operation.RequestBody = new OpenApiRequestBody
{
Required = true,
Content = new Dictionary<string, OpenApiMediaType>()
};
}
// Add multipart/form-data if missing
if (!operation.RequestBody.Content.ContainsKey("multipart/form-data"))
{
operation.RequestBody.Content.Add("multipart/form-data", new OpenApiMediaType
{
Schema = new OpenApiSchema
{
Type = "object",
Properties = new Dictionary<string, OpenApiSchema>
{
["file"] = new OpenApiSchema
{
Type = "string",
Format = "binary",
Description = "PDF file to upload"
}
},
Required = new HashSet<string> { "file" }
}
});
}
// Add application/json if missing
if (!operation.RequestBody.Content.ContainsKey("application/json"))
{
operation.RequestBody.Content.Add("application/json", new OpenApiMediaType
{
Schema = new OpenApiSchema
{
Type = "object",
Properties = new Dictionary<string, OpenApiSchema>
{
["base64Pdf"] = new OpenApiSchema
{
Type = "string",
Format = "byte",
Description = "Base64-encoded PDF file content"
}
},
Required = new HashSet<string> { "base64Pdf" }
}
});
}
}
}
}
}
}

View File

@@ -24,6 +24,13 @@ namespace DocumentOperator.API.Configuration
Description = "PDF Verarbeitungs-Service für Validierung, Stempel, Zertifikate, Anhänge & Zusammenführung"
});
// Resolve conflicting actions: Keep first variant
// DualInputDocumentFilter will merge both variants into single operation
options.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
// Add document filter to merge operations with different content types
options.DocumentFilter<DualInputDocumentFilter>();
// XML-Kommentare einbinden
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);