diff --git a/src/FrameProcessor/Program.cs b/src/FrameProcessor/Program.cs index 2806b3a..995c308 100644 --- a/src/FrameProcessor/Program.cs +++ b/src/FrameProcessor/Program.cs @@ -5,6 +5,7 @@ using FrameProcessor.Middleware; using FrameProcessor.Mqtt; using FrameProcessor.Storage; using FrameProcessor.UrlFetch; +using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.Extensions.Options; using Microsoft.OpenApi; using Serilog; @@ -50,6 +51,68 @@ builder.Services.AddOpenApi(options => ]; return Task.CompletedTask; }); + + // Microsoft.AspNetCore.OpenApi (10.0) does not emit a multipart/form-data + // request body for [FromForm] IFormFile parameters on MVC controllers, so + // Swagger UI renders no upload widget. Synthesize one here for any operation + // whose action takes form-bound parameters. + options.AddOperationTransformer((operation, context, _) => + { + var formParams = context.Description.ParameterDescriptions + .Where(p => p.Source == BindingSource.FormFile || p.Source == BindingSource.Form) + .ToList(); + + if (formParams.Count == 0) + { + return Task.CompletedTask; + } + + var properties = new Dictionary(); + var required = new HashSet(); + + foreach (var p in formParams) + { + var isFile = p.Source == BindingSource.FormFile + || typeof(IFormFile).IsAssignableFrom(p.ModelMetadata?.ModelType); + + properties[p.Name] = new OpenApiSchema + { + Type = JsonSchemaType.String, + Format = isFile ? "binary" : null, + }; + + if (p.IsRequired) + { + required.Add(p.Name); + } + } + + operation.RequestBody = new OpenApiRequestBody + { + Required = true, + Content = new Dictionary + { + ["multipart/form-data"] = new OpenApiMediaType + { + Schema = new OpenApiSchema + { + Type = JsonSchemaType.Object, + Properties = properties, + Required = required, + }, + }, + }, + }; + + if (operation.Parameters is { Count: > 0 }) + { + var formNames = formParams.Select(p => p.Name).ToHashSet(StringComparer.OrdinalIgnoreCase); + var filtered = operation.Parameters.Where(p => p.Name is null || !formNames.Contains(p.Name)).ToList(); + operation.Parameters = filtered.Count > 0 ? filtered : null; + } + + return Task.CompletedTask; + }); }); builder.Services.AddOptions()