ALang
ALang

Reputation: 73

Swagger listing IFormFile parameter as type "object"

I have a controller that requests a model containing an IFormFile as one of it's properties. For the request description, the Swagger UI (I'm using Swashbuckle and OpenApi 3.0 for .NET Core) lists the type of the file property as type object. Is there some way to make the Swagger UI denote the exact type and it's JSON representation to help the client?

The controller requesting the model looks as follows.

[HttpPost]
[Consumes("multipart/form-data")
public async Task<IActionResult> CreateSomethingAndUploadFile ([FromForm]RequestModel model)
{
    // do something
}

And the model is defined as below:

public class AssetCreationModel
{
    [Required}
    public string Filename { get; set; }

    [Required]
    public IFormFile File { get; set; }       
}

Upvotes: 4

Views: 10845

Answers (2)

psmitty
psmitty

Reputation: 303

We've been exploring this issue today. If you add the following to your startup it will convert IFormFile to the correct type

services.AddSwaggerGen(c => {
   c.SchemaRegistryOptions.CustomTypeMappings.Add(typeof(IFormFile), () => new Schema() { Type = "file", Format = "binary"});
});

Also see the following article on file upload in .net core https://learn.microsoft.com/en-us/aspnet/core/mvc/models/file-uploads?view=aspnetcore-2.1

Upvotes: 6

Razvan Dumitru
Razvan Dumitru

Reputation: 12452

This problem was already tackled in the following github issue/thread.

This improvement was already merged into Swashbuckle.AspNetCore master (as per 10/30/2018), but i don't expect that to be available as a package soon.

There are simple solutions if you only have a IFormFile as a parameter.

public async Task UploadFile(IFormFile filePayload){}

For simple case you can take a look at the following answer.

For complicated cases like container cases, you can take a look at the following answer.

internal class FormFileOperationFilter : IOperationFilter
{
    private struct ContainerParameterData
    {
        public readonly ParameterDescriptor Parameter;
        public readonly PropertyInfo Property;

        public string FullName => $"{Parameter.Name}.{Property.Name}";
        public string Name => Property.Name;

        public ContainerParameterData(ParameterDescriptor parameter, PropertyInfo property)
        {
            Parameter = parameter;
            Property = property;
        }
    }

    private static readonly ImmutableArray<string> iFormFilePropertyNames =
        typeof(IFormFile).GetTypeInfo().DeclaredProperties.Select(p => p.Name).ToImmutableArray();

    public void Apply(Operation operation, OperationFilterContext context)
    {
        var parameters = operation.Parameters;
        if (parameters == null)
            return;

        var @params = context.ApiDescription.ActionDescriptor.Parameters;
        if (parameters.Count == @params.Count)
            return;

        var formFileParams =
            (from parameter in @params
                where parameter.ParameterType.IsAssignableFrom(typeof(IFormFile))
                select parameter).ToArray();

        var iFormFileType = typeof(IFormFile).GetTypeInfo();
        var containerParams =
            @params.Select(p => new KeyValuePair<ParameterDescriptor, PropertyInfo[]>(
                p, p.ParameterType.GetProperties()))
            .Where(pp => pp.Value.Any(p => iFormFileType.IsAssignableFrom(p.PropertyType)))
            .SelectMany(p => p.Value.Select(pp => new ContainerParameterData(p.Key, pp)))
            .ToImmutableArray();

        if (!(formFileParams.Any() || containerParams.Any()))
            return;

        var consumes = operation.Consumes;
        consumes.Clear();
        consumes.Add("application/form-data");

        if (!containerParams.Any())
        {
            var nonIFormFileProperties =
                parameters.Where(p =>
                    !(iFormFilePropertyNames.Contains(p.Name)
                    && string.Compare(p.In, "formData", StringComparison.OrdinalIgnoreCase) == 0))
                    .ToImmutableArray();

            parameters.Clear();
            foreach (var parameter in nonIFormFileProperties) parameters.Add(parameter);

            foreach (var parameter in formFileParams)
            {
                parameters.Add(new NonBodyParameter
                {
                    Name = parameter.Name,
                    //Required = , // TODO: find a way to determine
                    Type = "file"
                });
            }
        }
        else
        {
            var paramsToRemove = new List<IParameter>();
            foreach (var parameter in containerParams)
            {
                var parameterFilter = parameter.Property.Name + ".";
                paramsToRemove.AddRange(from p in parameters
                                        where p.Name.StartsWith(parameterFilter)
                                        select p);
            }
            paramsToRemove.ForEach(x => parameters.Remove(x));

            foreach (var parameter in containerParams)
            {
                if (iFormFileType.IsAssignableFrom(parameter.Property.PropertyType))
                {
                    var originalParameter = parameters.FirstOrDefault(param => param.Name == parameter.Name);
                    parameters.Remove(originalParameter);

                    parameters.Add(new NonBodyParameter
                    {
                        Name = parameter.Name,
                        Required = originalParameter.Required,
                        Type = "file",
                        In = "formData"
                    });
                }
            }
        }
    }
}

You need to look into how you can add some/an OperationFilter that is suitable for your case.

Upvotes: 1

Related Questions