Swashbuckle UI problem when one of the input parameter is a file

Viewed 2300

I'm trying to upload a file with some metadata:

Upload(IFormFile file, [FromForm]IEnumerable<MetadataValue> list)

public class MetadataValue
{
    public Metadata Metadata { get; set; }
    public string Value { get; set; }
}

public enum Metadata
{
    Cat1,
    Cat2
}

A list of MetadataValue is displayed like an array of string inputs. I was not able to determine how should I pass the data to the controller - I'm always receiving an empty collection.

enter image description here

Everything is working when I'm sending a request from Postman. This is handled by dot pattern/notation.

enter image description here

In Postman each parameter and each property of a complex object is being sent as a separate field:

curl --location --request POST 'https://localhost:44395/api/document/upload' \
--form 'file=@/C:/FilterDraft.txt' \
--form 'list[0].metadata=Cat1' \
--form 'list[0].value=ABC' \
--form 'list[1].metadata=Cat2' \
--form 'list[1].value=DEF'

How can I achieve this in Swagger UI?
How can I configure Swagger for better UI generation?

I'm using:

  • .NET Core 3.1
  • Swashbuckle.AspNetCore 5.5.1
1 Answers

Solution:

To add examples you have to decorate the action method with SwaggerOperationFilter:

[SwaggerOperationFilter(typeof(OperationFilter))]
Upload(IFormFile file, [FromForm]IEnumerable<MetadataValue> list)
[...]
internal class UploadOperationFilter : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        if (operation.OperationId != nameof(DocumentController.Upload))
        {
            return;
        }
        if (operation.RequestBody.Content.TryGetValue("multipart/form-data", out var openApiMediaType))
        {
            var options = new JsonSerializerOptions { WriteIndented = true };
            options.Converters.Add(new JsonStringEnumConverter());

            var array = new OpenApiArray
            {
                new OpenApiString(JsonSerializer.Serialize(new MetadataValue {Metadata = Metadata.Cat1, Value = "ABC"}, options)),
                new OpenApiString(JsonSerializer.Serialize(new MetadataValue {Metadata = Metadata.Cat2, Value = "DEF"}, options))
            };

            openApiMediaType.Schema.Properties["metadata"].Example = array;
        }
    }
}

To get values in the controller (not empty collection), you have to add custom ModelBinder:

[ModelBinder(BinderType = typeof(MetadataValueModelBinder))]
public class MetadataValue
{
    public Metadata Metadata { get; set; }
    public string Value { get; set; }
}

public class MetadataValueModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
                throw new ArgumentNullException(nameof(bindingContext));

            var values = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

            if (values.Length == 0)
                return Task.CompletedTask;

            var options = new JsonSerializerOptions();
            options.Converters.Add(new JsonStringEnumConverter());

            var deserialized = JsonSerializer.Deserialize(values.FirstValue, bindingContext.ModelType, options);

            bindingContext.Result = ModelBindingResult.Success(deserialized);
            return Task.CompletedTask;
        }
    }
Related