Search code examples
c#.net-coreswagger-uiswashbuckleswashbuckle.aspnetcore

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


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

Solution


  • 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;
            }
        }