Swashbuckle.aspnetcore: Uploading a File using IFormFile Should Show an Upload Button

Created on 14 Oct 2016  路  55Comments  路  Source: domaindrivendev/Swashbuckle.AspNetCore

I am using IFormFile in the following action method to upload a file to my API. This causes the Swagger UI to display a text box. Is there a way to show an upload button in the Swagger UI to upload a file with the application/zip MIME type?

[HttpPost("{GuideId}/content", Name = GuidesControllerRouteName.PostZipFile)]
[Consumes("application/zip")]
[ProducesResponseType(typeof(void), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)]
public Task<IActionResult> PostZipFile([FromBody] IFormFile zipFile)
{
}

In addition, I've tried using the following IOperationFilter based on this MVC 5 Swashbuckle code without success:

public class FormFileSchemaFilter : IOperationFilter
{
    public void Apply(Operation operation, OperationFilterContext context)
    {
        if (context.ApiDescription.ParameterDescriptions.Any(x => x.Type == typeof(IFormFile)))
        {
            var bodyParameter = operation.Parameters.OfType<BodyParameter>().First();
            bodyParameter.In = "formData";
            bodyParameter.Required = true;
            bodyParameter.Schema.Type = "file";
        }
    }
}

Most helpful comment

I have written a more robust and generic version of FormFileOperationFilter. It supports:

  1. Just add it and it will do the right thing for any IFormFile parameter found.
  2. Other parameters can be used along with IFormFile.
  3. It adds the formData parameter in the correct order if you have other parameters.
  4. It adds the application/form-data MIME type only if required.

Stuff that's missing:

  1. Support for ICollection<IFormFile>.
  2. It does not pick up the formFile parameter comment from the action method but instead just hard-codes it as "The file to upload".

Perhaps this should be in a PR?

public class FormFileOperationFilter : IOperationFilter
{
    private const string FormDataMimeType = "multipart/form-data";
    private static readonly string[] FormFilePropertyNames =
        typeof(IFormFile).GetTypeInfo().DeclaredProperties.Select(x => x.Name).ToArray();

    public void Apply(Operation operation, OperationFilterContext context)
    {
        if (context.ApiDescription.ParameterDescriptions.Any(x => x.ModelMetadata.ContainerType == typeof(IFormFile)))
        {
            var formFileParameters = operation
                .Parameters
                .OfType<NonBodyParameter>()
                .Where(x => FormFilePropertyNames.Contains(x.Name))
                .ToArray();
            var index = operation.Parameters.IndexOf(formFileParameters.First());
            foreach (var formFileParameter in formFileParameters)
            {
                operation.Parameters.Remove(formFileParameter);
            }

            var formFileParameterName = context
                .ApiDescription
                .ActionDescriptor
                .Parameters
                .Where(x => x.ParameterType == typeof(IFormFile))
                .Select(x => x.Name)
                .First();
            var parameter = new NonBodyParameter()
            {
                Name = formFileParameterName,
                In = "formData",
                Description = "The file to upload.",
                Required = true,
                Type = "file"
            };
            operation.Parameters.Insert(index, parameter);

            if (!operation.Consumes.Contains(FormDataMimeType))
            {
                operation.Consumes.Add(FormDataMimeType);
            }
        }
    }
}

All 55 comments

Another thing I've tried is abandoning using IFormFile altogether and using a Stream like so:

[HttpPost("{GuideId}/content", Name = "PostZipFile")]
[Consumes("application/zip")]
[ProducesResponseType(typeof(void), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(void), StatusCodes.Status404NotFound)]
public Task<IActionResult> PostZipFile([FromBody] Stream stream)
{
}

In this case, according to the Swagger spec I'm not using a mulit-part form post so no need for an operation filter. However, this also shows a text box.

Janak S. has provided an excellent solution for this. I believe it should produce your desired results.
Solution

All credit goes to Janak!

My Example:

Controller


public async Task<IActionResult> UploadFile(IFormFile filePayload)
{
            var fileName = ContentDispositionHeaderValue
                .Parse(filePayload.ContentDisposition)
                .FileName
                .Trim('"');
            if (filePayload.Length > 0)
                using (var fileStream = new FileStream(Path.Combine($"{_config.GetValue<string>("FileUploadPath")}", fileName), FileMode.Create))
                    await filePayload.CopyToAsync(fileStream);
            return new OkObjectResult("Success");
}

Operation Filter


public class FileOperationFilter : IOperationFilter
    {
        public void Apply(Operation operation, OperationFilterContext context)
        {
            if (context.ApiDescription.ParameterDescriptions.Any(x => x.ModelMetadata.ContainerType == typeof(IFormFile)))
            {
                operation.Parameters.Clear();
                operation.Parameters.Add(new NonBodyParameter
                {
                    Name = "FilePayload", // must match parameter name from controller method
                    In = "formData",
                    Description = "Upload file.",
                    Required = true,
                    Type = "file"
                });
                operation.Consumes.Add("application/form-data");
            }
        }
    }

Swagger config


 services.AddSwaggerGen(c =>
            {
                c.OperationFilter<FileOperationFilter>();
            });

image

Thanks for that, most useful.

When should you use IFormFile over just using this.Request.Body.Stream? What are the advantages of using IFormFile. I can understand using IFormFile to upload files in an MVC web app but what is the correct method of uploading files using ASP.NET Core when writing an API that would be consumed by a mobile device for example.

According to the Swagger specs, Type = "file" is only relevant for application/form-data. Is it possible to get a file upload button using this.Request.Body.Stream?

I have written a more robust and generic version of FormFileOperationFilter. It supports:

  1. Just add it and it will do the right thing for any IFormFile parameter found.
  2. Other parameters can be used along with IFormFile.
  3. It adds the formData parameter in the correct order if you have other parameters.
  4. It adds the application/form-data MIME type only if required.

Stuff that's missing:

  1. Support for ICollection<IFormFile>.
  2. It does not pick up the formFile parameter comment from the action method but instead just hard-codes it as "The file to upload".

Perhaps this should be in a PR?

public class FormFileOperationFilter : IOperationFilter
{
    private const string FormDataMimeType = "multipart/form-data";
    private static readonly string[] FormFilePropertyNames =
        typeof(IFormFile).GetTypeInfo().DeclaredProperties.Select(x => x.Name).ToArray();

    public void Apply(Operation operation, OperationFilterContext context)
    {
        if (context.ApiDescription.ParameterDescriptions.Any(x => x.ModelMetadata.ContainerType == typeof(IFormFile)))
        {
            var formFileParameters = operation
                .Parameters
                .OfType<NonBodyParameter>()
                .Where(x => FormFilePropertyNames.Contains(x.Name))
                .ToArray();
            var index = operation.Parameters.IndexOf(formFileParameters.First());
            foreach (var formFileParameter in formFileParameters)
            {
                operation.Parameters.Remove(formFileParameter);
            }

            var formFileParameterName = context
                .ApiDescription
                .ActionDescriptor
                .Parameters
                .Where(x => x.ParameterType == typeof(IFormFile))
                .Select(x => x.Name)
                .First();
            var parameter = new NonBodyParameter()
            {
                Name = formFileParameterName,
                In = "formData",
                Description = "The file to upload.",
                Required = true,
                Type = "file"
            };
            operation.Parameters.Insert(index, parameter);

            if (!operation.Consumes.Contains(FormDataMimeType))
            {
                operation.Consumes.Add(FormDataMimeType);
            }
        }
    }
}

@RehanSaeed - I would like to get better support for file uploads out-of-the-box. If it goes into the main code though, it might make more sense to add to the SwaggerGenerator instead of as an operation filter.

If you're interested in creating a PR that would be awesome - I'll mark it down for the 6.1 milestone and we can discuss the implementation details further. For now, I'm focused on getting 6.0.0 out.

Thanks for the support

@domaindrivendev Will see about the PR if I get time. I realized that my code does not work if I create a model class which contains a IFormFile property. How do you handle this elsewhere?

Creating a model class is important if you want to write a custom validator attribute to validate IFormFile's to check for zero byte files, files too large and correct MIME types. The alternative is to add this validation logic in my action method which is ugly.

@RehanSaeed, try聽this

@domaindrivendev, IFormFile is a general case, and practicing shamanism with聽IOperationFilter for this general case is not what all of us want.
Do you have plans of聽adding this聽case聽or would you like a pull request from us?

@xperiandri - yes it's a known issue that I would like to address and yes I would like a PR. I'm always interested in PR's so long as they come with tests.

Ideally this would live in the SwaggerGenerator. However, there's some behavior in ApiExplorer that makes this change more difficult than in should be - that is, it flattens out complex type properties for all non-body (i.e. no FromBodyAttribute) action parameters into multiple ApiParameterDescriptions. I've submitted an issue to the ASP.NET Core folks and it sounds like they might take a look at improving things - https://github.com/aspnet/Mvc/issues/5673.

So, in the meantime, I'd be open to fixing Swashbuckle by wiring up an OperationFilter similar to yours out-of-the-box. Similar to the Annotations filters already there. Sounds like your one is close but still doesn't address the scenario where a class contains an IFormFile property. That and a few tests and we'd be good to go. Let me know if you have some cycles to look into this. Thanks for your support

Yep, I didn't test it on a model with聽IFormFile property

@RehanSaeed
I had to modify the filter a little bit to mix FromQuery parameters with IFormFile
The Query parameter may clash with file form fields. 'Name' for example.

I had to add

.Where(x => x.In == "form")

in

var formFileParameters = operation
                    .Parameters
                    .OfType<NonBodyParameter>()
                    .Where(x => x.In == "form")
                    .Where(x => FormFilePropertyNames.Contains(x.Name))
                    .ToArray();
                var index = operation.Parameters.IndexOf(formFileParameters.First());

I've looked at all attempts in this issue and in #261 and combined the attempts to this OperationFilter.
To be clear, it supports a IFormFile property as an action parameter, a class as action parameter containing an IFormFile property. I've also fixed the bug the @jonnybi mentioned.

Setting the required property is not the responsibility of this filter IMO, it should be set with the required attribute because it might be optional. So I've removed that part from the filter.

If you guys agree I will write some tests and create a PR. Hopefully I'll have some time the coming week or so to write the tests.

``` c#
///


/// Adds support for IFormFile parameters in Swashbuckle.
///

public class FormFileOperationFilter : IOperationFilter
{
// TODO: Support ICollection

private const string formDataMimeType = "multipart/form-data";
private static readonly string[] formFilePropertyNames =
    typeof(IFormFile).GetTypeInfo().DeclaredProperties.Select(p => p.Name).ToArray();

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

    var formFileParameterNames = new List<string>();
    var formFileSubParameterNames = new List<string>();

    foreach (var actionParameter in context.ApiDescription.ActionDescriptor.Parameters)
    {
        var properties =
            actionParameter.ParameterType.GetProperties()
                .Where(p => p.PropertyType == typeof(IFormFile))
                .Select(p => p.Name)
                .ToArray();

        if (properties.Length != 0)
        {
            formFileParameterNames.AddRange(properties);
            formFileSubParameterNames.AddRange(properties);
            continue;
        }

        if (actionParameter.ParameterType != typeof(IFormFile)) continue;
        formFileParameterNames.Add(actionParameter.Name);
    }

    if (!formFileParameterNames.Any()) return;

    var consumes = operation.Consumes;
    consumes.Clear();
    consumes.Add(formDataMimeType);

    foreach (var parameter in parameters.ToArray())
    {
        if (!(parameter is NonBodyParameter) || parameter.In != "formData") continue;

        if (formFileSubParameterNames.Any(p => parameter.Name.StartsWith(p + "."))
            || formFilePropertyNames.Contains(parameter.Name))
            parameters.Remove(parameter);
    }

    foreach (var formFileParameter in formFileParameterNames)
    {
        parameters.Add(new NonBodyParameter()
        {
            Name = formFileParameter,
            Type = "file",
            In = "formData"
        });
    }
}

}
```

@nphmuller, could you explain this private const string formDataMimeType = "application/form-data"; in detail, please?
What if a few IFormFile parameters will exist or collection of them?

Which options of putting IFormFile work at all?

  1. Collection of IFormFile IActionResult WebMethod(IFormFile[] files)
  2. Files as parameters IActionResult WebMethod(IFormFile file1, IFormFile file1)
  3. Single object that contains IFormFile properties IActionResult WebMethod(FileContainer container)

Will MVC resolve anything else?

@xperiandri : About the mime type. I tried to reproduce the problem again, but I couldn't. So I might just have made a typo or something the first round. I'll change it back to multipart/form-data in my previous post.

Now about the scenarios:

  • IFormFile[], IEnumerable
  • This is supported (even the combination of these parameters in a single request):
    ``` c#
    public class Request
    {
    public IFormFile File1 { get; set; }
    public IFormFile File2 { get; set; }
    }

public class Controller
{
[HttpPost("Upload")]
public IActionResult Upload([FromForm] Request request, [FromForm] IFormFile file3, [FromForm] IFormFile file4, [FromQuery] string FileName)
{
....
}
}
```

Note the query parameter which would get removed from the swagger docs if the following line was missing from the OperationFilter (the bug @jonnybi mentioned): if (!(parameter is NonBodyParameter) || parameter.In != "formData") continue;

By the way, I just did a quick check on supporting IFormFileCollection, but it seems v2 of the swagger spec doesn't support a multiple file parameter type. So every implementation would be a workaround for that.
See: https://github.com/swagger-api/swagger-ui/issues/823

Implemented all scenarios except mixed (top level parameter and container).
The only thing I can't figure out is marked with TODO

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

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

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

private class ParameterByNameComparison : IComparer<IParameter>
{
    public int Compare(IParameter x, IParameter y) => string.Compare(x.Name, y.Name);
}

private static readonly IComparer<IParameter> comparer = new ParameterByNameComparison();

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))
                parameters.Add(new NonBodyParameter
                {
                    Name = parameter.Name,
                    Required = IsRequired(parameter.Property),
                    Type = "file"
                });
            else
            {
                var indexesOfTopLevelParam = @params
                    .Zip(Enumerable.Range(0, @params.Count), (p, i) => new KeyValuePair<ParameterDescriptor, int>(p, i))
                    .Where(pi => pi.Key.Name == parameter.Property.Name)
                    .Select(pi => pi.Value);
                int skipIndex;
                if (indexesOfTopLevelParam.Any())
                    skipIndex = indexesOfTopLevelParam.First();
                else
                    skipIndex = 0;

                var filteredParameters = parameters
                    .Where(p => p.Name == parameter.Property.Name
                             || p.Name.EndsWith("." + parameter.Property.Name))
                    .ToList();
                filteredParameters.RemoveAt(skipIndex);
                filteredParameters.First(p => p.Name == parameter.Property.Name)
                                  .Name = parameter.Name;
            }
        }

    }
    foreach (IParameter param in parameters)
    {
        param.In = "formData";
    }
    (parameters as List<IParameter>)?.Sort(comparer);
}

private static bool IsRequired(PropertyInfo propertyInfo)
    => propertyInfo.CustomAttributes
                   .OfType<RequiredAttribute>()
                   .Any();

}

Test controller
``` C#
[ApiVersionNeutral]
[Route("[controller]/[action]")]
public class FormFileController : Controller
{
    [HttpPost]
    public IActionResult Array([FromForm] IFormFile[] files, [FromForm] string name)
    {
        return Ok();
    }

    [HttpPost]
    public IActionResult IList([FromForm] IList<IFormFile> files, [FromForm] string name)
    {
        return Ok();
    }

    [HttpPost]
    public IActionResult TwoFiles([FromForm] IFormFile file1, [FromForm] IFormFile file2, [FromForm] string name)
    {
        return Ok();
    }

    [HttpPost]
    public IActionResult Container([FromForm] Container container, [FromForm] string Property)
    {
        return Ok();
    }

    [HttpPost]
    public IActionResult DoubleContainer([FromForm] DoubleContainer container, [FromForm] string Property)
    {
        return Ok();
    }

    [HttpPost]
    public IActionResult TwoContainers([FromForm] Container container1, [FromForm] Container container2, [FromForm] string Property)
    {
        return Ok();
    }
}

public class Container
{
    public int Property { get; set; }
    public IFormFile File { get; set; }
}

public class DoubleContainer
{
    public int Property { get; set; }
    public IFormFile File1 { get; set; }
    public IFormFile File2 { get; set; }
}

@xperiandri It's getting pretty complicated, but it seems to be working fine, with the exception of a couple of things:

  • Combining a complex object and IFormFile doesn't work properly (See x1)
  • Swagger UI doesn't work properly for a IFormFile collection. (Ok for me, since it's not supported anyway.)
  • IFormFileCollection doesn't work. But as a workaround a collection of IFormFile objects can be used.

Combining an IFormFile collection and a single IFormFile parameter in an controller action also works. Which is really nice. :)
I.e.: IActionResult Upload(IFormFile[] files, IFormFile file)

x1:
``` c#
public class Request
{
public IFormFile File1 { get; set; }
}
public IActionResult Upload([FromForm] Request request, [FromForm] IFormFile file2)

Some other feedback:
- CustomAttributes.OfType<RequiredAttribute> is never going to give any results, because RequiredAttribute is not of the type CustomAttributeData (which is the collection type you're calling OfType() on.
- In your struct the Parameter property is never used. So you could use the Property property directly, instead of the struct.

In my code I have made a special filter to determine if an action parameter is required or not. It might help you with your todo:

``` c# 
    /// <summary>
    /// Checks if a controller action parameter is decorated with a <see cref="RequiredAttribute"/> and
    /// adds this requirements to the swagger docs.
    /// </summary>
    public class RequiredParameterOperationFilter : IOperationFilter
    {
        public void Apply(Operation operation, OperationFilterContext context)
        {
            var actionParameters = context.ApiDescription.ActionDescriptor.Parameters.OfType<ControllerParameterDescriptor>();
            foreach (var actionParameter in actionParameters)
            {
                // Complex Form parameters are unpacked in swagger.
                // Try to map their properties to the unpacked variant.
                foreach (var property in actionParameter.ParameterType.GetProperties())
                    CheckIfRequired(operation, property, property.Name);

                CheckIfRequired(operation, actionParameter.ParameterInfo, actionParameter.Name);
            }
        }

        private void CheckIfRequired(Operation operation, ICustomAttributeProvider parameterType, string parameterName)
        {
            if (!parameterType.GetCustomAttributes(typeof(RequiredAttribute), false).Any()) return;
            var operationParameter = operation.Parameters.SingleOrDefault(p => p.Name == parameterName);
            if (operationParameter == null) return;
            operationParameter.Required = true;
        }
    }

If an operation contains parameters, that don't send in form (path or query parameters), it doesn't work:
foreach (IParameter param in parameters)
{
param.In = "formData";
}
I think this block should be removed and "param.In=..." should be moved here:
parameters.Add(new NonBodyParameter
{
Name = parameter.Name,
//Required = , // TODO: find a way to determine
Type = "file",
In = "formData"
});

foreach (IParameter param in parameters) { param.In = param.In==null? "formData" : param.In; } (parameters as List<IParameter>)?.Sort(comparer);

This will fix it

@xperiandri @nphmuller

I used the code from https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/193#issuecomment-280808098 and I have tried to use IFormFile[] and IList<IFormFile> but it does not seem to work as I would expect. There is no file upload button shown in the swagger ui. I know the swagger spec does not support multiple file uploads anyways, but it would be nice to at least be able to upload a single file over the ui and multiple files not using the ui.

image

This is what i did to show multiple file updloads:

[AttributeUsage(AttributeTargets.Method, AllowMultiple =true)]
    public sealed class SwaggerFormParameter : Attribute
    {
        public string Name { get; private set; }
        public string Type { get; private set; }
        public string Description { get; set; }
        public bool IsRequired { get; set; }

        public SwaggerFormParameter(string name, string type)
        {
            Name = name;
            Type = type;
        }

    }
public class ImportFileParamType : IOperationFilter
    {
        public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
        {
            var requestAttributes = apiDescription.GetControllerAndActionAttributes<SwaggerFormParameter>();
            foreach (var attr in requestAttributes)
            {
                if (operation.parameters == null)
                    operation.parameters = new List<Parameter>();

                if(operation.consumes.Count == 0)
                    operation.consumes.Add("multipart/form-data");

                operation.parameters.Add(
                    new Parameter
                    {
                        name = attr.Name,
                        description = attr.Description,
                        @in = "formData",
                        required = attr.IsRequired,
                        type = attr.Type,
                    });

            }
        }
    }
[HttpPost]
        [SwaggerFormParameter(RecipientFormField, "string", Description = "Email Sender")]
        [SwaggerFormParameter(PdfFileFormField, "string", Description = "Pdf Filename")]
        [SwaggerFormParameter("image1", "file", Description = "Bild 1")]
        [SwaggerFormParameter("image2", "file", Description = "Bild 2")]
        [SwaggerFormParameter("image3", "file", Description = "Bild 3")]
        [SwaggerFormParameter("image4", "file", Description = "Bild 4")]
        [SwaggerFormParameter("image5", "file", Description = "Bild 5")]
        [SwaggerFormParameter("image6", "file", Description = "Bild 6")]
        [SwaggerFormParameter("image7", "file", Description = "Bild 7")]
        [SwaggerFormParameter("image8", "file", Description = "Bild 8")]
        [SwaggerFormParameter("image9", "file", Description = "Bild 9")]
        [SwaggerFormParameter("image10", "file", Description = "Bild 10")]
        public async Task<HttpResponseMessage> CreatePdfAnsSendEmail()

image

Allthough this was not a .net core project it should be working in core too.

@jonnybi Thanks for your solution! Although that is not exactly what I am looking for. I would expect the swagger ui to have a single file selection input field, with which I could select multiple images in the browser window that opens. But I guess that is not possible at the moment. That makes your solution probably the only solution possible to support multiple files by adding a fixed amount of upload fields.

For multiple file upload you would want to set the multiple attribute on the input element.
I.e. <input type="file" multiple />
However, there is no way to set the multiple attribute (or an arbitrary attribute) on the input element using the NonBodyParameter class or the PartialSchema class.

Issue with versioning on suggested solution

Can't wait for this to be included 馃槃

I tried to follow https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/193#issuecomment-280808098 with an odd result. I have mine set up with versioning. What happens is when I go to submit I get a URL like this

http://localhost:60603/aim/v%7Bversion%7D/write/locations/import/file

where I was expecting, and all of the other routes correctly produce,

http://localhost:60603/aim/v1/write/locations/import/file

I will work through this (although pointers would be awesome) but wanted to provide feed back on this.

MINOR EDIT

just back tracking everything, I can't seem to find where it drops out so any help would be great. What i do see is that is the marked in the parameters for Path with the correct name by the time it hits consumes.Add("application/form-data");, it just is not replacing the version out so any posting gets the funny URL.

@jeremyBass, see versioning samples.
You can go 2 ways:

  1. As in versioning sample, treat version as parameter
  2. As posted here, where parameter removed and replaced by real value

Then just apply document filter that rewrites URLs

C# public class SetVersionInPaths : IDocumentFilter { public void Apply(SwaggerDocument swaggerDoc, DocumentFilterContext context) { swaggerDoc.Paths = swaggerDoc.Paths .ToDictionary( path => path.Key.Replace("v{version}", swaggerDoc.Info.Version), path => path.Value ); } }
I ended up with adding just this filter to versioning sample

@xperiandri That is a great way to go. Works like a charm.

I made my own filter before finding this thread :(

Anyway maybe someone will find it useful for uploading files which can be a part of a more complex input parameter (no attributes or flags used, so should work out of the box in most cases *I think ;) )

https://gist.github.com/theCuriousOne/39b79e40e3e7194b7bdb66cc910ee1b4

(just made it, haven't tested everything)

@xperiandri I fixed your Container Implementation:
```c#
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"
                });
            }
        }
    }
}

}
```

@WilliamABradley, could you highlight your changes?

@xperiandri Here is a diff:
http://mergely.com/s63u01ly/

Doesn't my version work?
Now I look at else clause and feel like I miss something

The else clause didn't seem necessary. With the container based version, it would break swagger generation. The changes I made fix the replacement of the parameter.
Also, required now works, as it gets it from the old parameter it replaces.

I had to do a following change to @WilliamABradley's code:

parameters.Add(new NonBodyParameter
{
    Name = parameter.Name,
    Required = originalParameter?.Required ?? true, // Otherwise I was gettting NullException
    Type = "file",
    In = "formData"
});

Any chance to include any of this in the official version? The default UX around IFormFile is very bad right now, any improvement would be useful even if it won't cover all the possible cases...

When using IFormFile, isn't anyone having trouble with validating the Swagger JSON in the editor ?

I just created an OperationFilter that replaces the TextBox for the Browse Button in every IFormFile parameter. It also keeps the main attributes (name, description, required) of the original parameter unchanged:

public class FileOperationFilter : IOperationFilter
    {
        public void Apply(Operation operation, OperationFilterContext context)
        {                        
            for (var i = 0; i < context.ApiDescription.ActionDescriptor.Parameters.Count; i++)
            {
                if (context.ApiDescription.ActionDescriptor.Parameters[i].ParameterType == typeof(IFormFile))
                {
                    var parameter = operation.Parameters.FirstOrDefault(p => p.Name.Equals(operation.Parameters[i].Name, StringComparison.OrdinalIgnoreCase));
                    if (parameter == null) return;

                    // remove o par芒metro
                    operation.Parameters.Remove(parameter);

                    // insere o novo par芒metro modificado
                    var fileParam = new NonBodyParameter
                    {
                        Type = "file",
                        In = "formData",
                        Description = parameter.Description,
                        Name = parameter.Name,
                        Required = parameter.Required,                        
                    };
                    operation.Parameters.Insert(i, fileParam);
                    operation.Consumes.Add("multipart/form-data");
                }
            }                      
        }
    }

On AddSwaggerGen, add:

services.AddSwaggerGen(c =>
{
  c.OperationFilter<FileOperationFilter>();
}

Is anyone work out multiple files upload? I don't known how to add

multiple="multiple"

attribute to the input tag.

Moving to milestone 4.0.0 as ASP.NET Core 2.x has updates to simplify the implementation. That's the milestone where I plan on introducing ASP.NET Core 2.x as a lower bound.

@jwilbor, you have this line:

var parameter = operation.Parameters.FirstOrDefault(p => p.Name.Equals(operation.Parameters[i].Name, StringComparison.OrdinalIgnoreCase));

where it should be:

var parameter = operation.Parameters.FirstOrDefault(p => p.Name.Equals(context.ApiDescription.ActionDescriptor.Parameters[i].Name, StringComparison.OrdinalIgnoreCase));

Otherwise, if the method gets multiple parameters and not just the file, you will override the wrong one.

Besides, thanks for sharing!

Support for parameters and properties of type IFormFile now merged into master. Will be generally available with the upcoming 4.0.0 release. NOTE: IFormFileCollection and IEnumerable<IFormFile> is still not supported as Swagger 2.0 doesn't support an array of file parameters (see https://github.com/swagger-api/swagger-ui/issues/823 for more details).

You can try it now with the latest preview package on myget.org:
https://www.myget.org/feed/domaindrivendev/package/nuget/Swashbuckle.AspNetCore

http://www.talkingdotnet.com/how-to-upload-file-via-swagger-in-asp-net-core-web-api/

post api/values/upload

public class SaveFileParam 
    {
        public string UniqueId { get; set; }
        public IFormFile File { get; set; }
    }
[HttpPost]
[Route("upload")]
public void PostFile(SaveFileParam fileWithComplexobject)
{
   //TODO: Save file
}



md5-00b10d78b6e6190991c6b55bc602b74d



public class FileUploadOperation : IOperationFilter
{
    public void Apply(Operation operation, OperationFilterContext context)
    {
        if (operation.OperationId.ToLower() == "apivaluesuploadpost")
        {
            operation.Parameters.Clear();
           operation.Parameters.Add(new NonBodyParameter
                {
                    Name = "File",
                    In = "formData",
                    Description = "Upload File",
                    Required = false,
                    Type = "file"
                });
                operation.Parameters.Add(new NonBodyParameter
                {
                    Name = "UniqueId",
                    In = "formData",
                    Description = "Uniquie id",
                    Required = false,
                    Type = "string"
                });
            operation.Consumes.Add("multipart/form-data");
        }
    }
}



md5-f18052d367fa13c3be9d991d585fc45c



services.AddSwaggerGen();
services.ConfigureSwaggerGen(options =>
{
    options.SingleApiVersion(new Info
    {
        Version = "v1",
        Title = "My API",
        Description = "My First Core Web API",
        TermsOfService = "None",
        Contact = new Contact() { Name = "Talking Dotnet", Email = "[email protected]", Url = "www.talkingdotnet.com" }
    });
    options.IncludeXmlComments(GetXmlCommentsPath());
    options.DescribeAllEnumsAsStrings();
    options.OperationFilter<FileUploadOperation>(); //Register File Upload Operation Filter
});

Hope this might helps enjoy :) 馃憤

@domaindrivendev Awesome to see this merged into master. Is there an expected timeline for 4.0.0 to be released.

If the answer is "a long time away", is there any chance of getting this filter put up as a nuget plugin, so that we can "install" it into existing 3.0 versions?

@mail2sarathee Whilst I'm sure the effort is much appreciated by all, that implementation has significant flaws (hardcoded to a certain API endpoint, Arbitrarily changes the paramter name, wipes all other parameters, adds a UniqueId for no obvious reason) and this thread is a detailed discussion of how to implement the feature properly (with a final solution reached).

IMO your comment is actively detrimental to this thread and future programmers coming to read it.

Please could you delete it, to avoid confusion for future devs.

Hello! I'm glad this thread is recently active ;-)

I'm trying to post a file as a property within a model.
It comes through as null or "[object] Object" depending on the file's property type.
Also, the following warning is output to the web server console (ServiceStack, but maybe relevant in this case?) ;

We're using Swashbuckle.AspNetCore (4.0.1). Do we need to downgrade?

warn: ServiceStack.Serialization.StringMapTypeDeserializer[0]
      Property 'thedocumentasstream' does not exist on type 'SubThingPostModel'
warn: ServiceStack.Serialization.StringMapTypeDeserializer[0]
      Could not create instance on 'SubThingPostModel' for property 'TheDocumentAsFormFile' with text value '[object File]'

Here's the model

[Api("Adds a document to the given TopThing")]
[Route("/TopThings/{TopThingId}/SubThings", "POST")]
public class SubThingPostModel
{
    [ApiMember(IsRequired = true, ParameterType = "path")]
    public int TopThingId { get; set; }

    //https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/193
    [ApiMember(IsRequired = true, ParameterType = "formData", DataType = "file")]
    public string TheDocumentAsString { get; set; }

    [ApiMember(IsRequired = true, ParameterType = "formData", DataType = "file")]
    public Stream TheDocumentAsStream { get; set; }

    [ApiMember(IsRequired = true, ParameterType = "formData", DataType = "file")]
    public IFormFile TheDocumentAsFormFile { get; set; }

    [ApiMember(IsRequired = true, ParameterType = "formData", DataType = "file")]
    public object TheDocumentAsObject { get; set; }
}

Here's the action

public int Post([FromForm] SubThingPostModel subThingPostModel)
{
    //[FromForm] doesn't change anything

    //subThingPostModel.TopThingId is as expected
    //subThingPostModel.TheDocumentAsString == "[object] Object"
    //subThingPostModel.TheDocumentAsStream == null
    //subThingPostModel.TheDocumentAsFormFile == null
    //subThingPostModel.TheDocumentAsObject == "[object] Object"
    return 0;
}

The IFormFile property matches a test case posted by @xperiandri on 17Feb

Our pattern doesn't match any of the samples I've found while researching my issue. Perhaps we are using a bad pattern?

-Mike

Here's the UI
image

Support for parameters and properties of type IFormFile now merged into master. Will be generally available with the upcoming 4.0.0

This doesn't seem to be working out of the box. Tried with both latest release and preview versions.

With [FromForm] it just generates a bunch of fields like _ContentType_, _ContentDisposition_, etc.
Without - generates a textbox for JSON input.

Can anybody please provide a working example without implementing custom OperationFilter or was support for this dropped along the way?

Hey, I'm also on Swashbuckle 4.0.1 and I have no clue how to get this working. I have the following endpoint method.

[HttpPost]
public IActionResult Post([FromForm] IFormFile file) { ... }

And it's still showing the old gnarly parameter list.

image


I have now become aware of https://github.com/domaindrivendev/Swashbuckle.AspNetCore/issues/1183 and have removed the [FromForm] attribute from the parameter. Things seem to work now.


Hopefully, this helps someone else. The reason I had [FromForm] on that parameter was because it's required unless you're also using the correct "compatibility version" or if you're on ASP.NET Core 3.x. Without one of these approaches, I was getting 415 Unsupported Media Type.

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);

Doesn't work for me at all, with or without fromform.

Im using 5.0-rc4 with .net core 3 and removing fronform fixed it for me. It is now showing a file upload button.

Had to upgrade to the 5.0 rc4 before it would work. Monumental pain though, as I had to break practically everything to do so.

note: this still will not work if you have SerializeAsV2 = true;
you must be using OpenApi 3.0 for this to work correctly.

note: this still will not work if you have SerializeAsV2 = true;
you must be using OpenApi 3.0 for this to work correctly.

You can add:
c.MapType(typeof(IFormFile), () => new OpenApiSchema() { Type = "file", Format = "binary" });
To AddSwaggerGen
And this will work with SerializeAsV2 = true

public class FormFileOperationFilter : IOperationFilter
{
private const string FormDataMimeType = "multipart/form-data";
private static readonly string[] FormFilePropertyNames =
typeof(IFormFile).GetTypeInfo().DeclaredProperties.Select(x => x.Name).ToArray();

public void Apply(Operation operation, OperationFilterContext context)
{
    if (context.ApiDescription.ParameterDescriptions.Any(x => x.ModelMetadata.ContainerType == typeof(IFormFile)))
    {
        var formFileParameters = operation
            .Parameters
            .OfType<NonBodyParameter>()
            .Where(x => FormFilePropertyNames.Contains(x.Name))
            .ToArray();
        var index = operation.Parameters.IndexOf(formFileParameters.First());
        foreach (var formFileParameter in formFileParameters)
        {
            operation.Parameters.Remove(formFileParameter);
        }

        var formFileParameterName = context
            .ApiDescription
            .ActionDescriptor
            .Parameters
            .Where(x => x.ParameterType == typeof(IFormFile))
            .Select(x => x.Name)
            .First();
        var parameter = new NonBodyParameter()
        {
            Name = formFileParameterName,
            In = "formData",
            Description = "The file to upload.",
            Required = true,
            Type = "file"
        };
        operation.Parameters.Insert(index, parameter);

        if (!operation.Consumes.Contains(FormDataMimeType))
        {
            operation.Consumes.Add(FormDataMimeType);
        }
    }
}

}

This example doesn't compile...

note: this still will not work if you have SerializeAsV2 = true;
you must be using OpenApi 3.0 for this to work correctly.

You can add:
c.MapType(typeof(IFormFile), () => new OpenApiSchema() { Type = "file", Format = "binary" });
To AddSwaggerGen
And this will work with SerializeAsV2 = true

This totally doesn't work unfortunately

None of the solutions posted on this issue actually work. Why is this issue closed?

@Cloudmersive The file upload worked for me out-of-the-box. The only thing that did not work properly is the multiple file upload since swagger adds an index after the property name (ex. Document1, Document2....) and that doesn't get bind with List. But this is an issue with swagger not swashbuckle.aspnetcore, so you might want to open an issue there (or better yet open a PR)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

govin picture govin  路  3Comments

NinoFloris picture NinoFloris  路  3Comments

brucewilkins picture brucewilkins  路  3Comments

voroninp picture voroninp  路  3Comments

mrmartan picture mrmartan  路  3Comments