Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

how to get swashbuckle to generate parameter type "form" for testing APIs that need a file upload? #120

Closed
sladeVol opened this issue Nov 12, 2014 · 15 comments

Comments

@sladeVol
Copy link

I've been trying to figure this out for the last few days. I am writing an API that uploads a file (based it a upload file tutorial on the asp.net/web-api site) , but I cannot figure out how to list the file parameter in the signature so that swashbuckle can generate the appropriate parameter type for the swagger ui to render a "select a file" button instead of a text field. Is this a limitation to web api, swashbuckle or to .Net in general?

@domaindrivendev
Copy link
Owner

I'm assuming your file upload implementation is similar to this - http://www.asp.net/web-api/overview/advanced/sending-html-form-data,-part-2. Because all the "file upload" specifics are happening at run-time, there is no way for Swashbuckle to automatically generate a Parameter description with the relevant "file upload" details.

So, you need to tell it about this specific parameter. This can be easily done with a custom IOperationFilter which you can wire up as follows:

AddFileParamTypes.cs

public class AddFileParamTypes : IOperationFilter
{
    public void Apply(Operation operation, DataTypeRegistry dataTypeRegistry, ApiDescription apiDescription)
    {
        if (operation.Nickname == "FileUpload_PostFormData") // controller and action name
        {
            operation.Consumes.Add("multipart/form-data");
            operation.Parameters.Add(new Parameter
                {
                    Name = "file",
                    Required = true,
                    Type = "file",
                    ParamType = "form"
                }
            );
        }
    }
}

SwaggerConfig.cs

SwaggerSpecConfig.Customize(c =>
    {
        c.OperationFilter<AddFileParamTypes>();
        ...

@wadewegner
Copy link

Looks like IOperationFilter changed since the comments above. Here's the code that worked for me:

public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
{
    if (operation.operationId == "FileUpload_PostFormData")  // controller and action name
    {
        operation.consumes.Add("multipart/form-data");
        operation.parameters = new List<Parameter>
        {
            new Parameter
            {
                name = "file",
                required = true,
                type = "file",
            }
        };
    }
}

@centur
Copy link

centur commented Oct 18, 2015

The comment above is no longer valid. See #531 issue (and mentioned there #280)
Changing code to:

new Parameter
  {
    name = "file",
    @in = "formData",
    required = true,
    type = "file"
  }

will fix the issue.
It's worth to note that working FileUpload operation filter can be found here

@RehanSaeed
Copy link

RehanSaeed commented Oct 17, 2016

Any ideas how to get an upload file button working in Ahoy (Swagger for ASP.NET Core). The above code does not do the trick. I raised the following issue domaindrivendev/Swashbuckle.AspNetCore#193

@glyons
Copy link

glyons commented Mar 23, 2017

Using an attriubute so it can be added to more controllers

public class ImportFileParamType : IOperationFilter
{
[AttributeUsage(AttributeTargets.Method)]
public sealed class SwaggerFormAttribute : Attribute
{
public SwaggerFormAttribute(string name, string description)
{
Name =name;
Description = description;
}
public string Name { get; private set; }

        public string Description { get; private set; }
    }
    public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
    {
        var requestAttributes = apiDescription.GetControllerAndActionAttributes<SwaggerFormAttribute>();
        foreach (var attr in requestAttributes)
        {
                operation.parameters = new List<Parameter>
                {
                    new Parameter
                    {
                        description = attr.Description,
                        name = attr.Name,
                        @in = "formData",
                        required = true,
                        type = "file",
                    }
                };
                operation.consumes.Add("multipart/form-data");
        }
    }`

API Controller

` [Route("ImageUpload")]
[HttpPost]
[ImportFileParamType.SwaggerFormAttribute("ImportImage", "Upload image file")]
public async Task PostFormDataImage()
{

        // Check if the request contains multipart/form-data.
        if (!Request.Content.IsMimeMultipartContent())
        {
            throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
        }

        string root = System.Web.HttpContext.Current.Server.MapPath("~/App_Data");
        var provider = new MultipartFormDataStreamProvider(root);

        try
        {
            // Read the form data.
            await Request.Content.ReadAsMultipartAsync(provider);

            // This illustrates how to get the file names.
            foreach (MultipartFileData file in provider.FileData)
            {
                Trace.WriteLine(file.Headers.ContentDisposition.FileName);
                Trace.WriteLine("Server file path: " + file.LocalFileName);
            }
            return Request.CreateResponse(HttpStatusCode.OK);
        }
        catch (System.Exception e)
        {
            return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, e);
        }
    }`

@razonrus
Copy link

razonrus commented Apr 8, 2017

Thank you @glyons !
A fix for the case where you have other options besides the file, for example your url is /api/user/{id}/photo:

foreach (var attr in requestAttributes)
            {
                operation.parameters = operation.parameters ?? new List<Parameter>();
                operation.parameters.Add(new Parameter
                {
                    description = attr.Description,
                    name = attr.Name,
                    @in = "formData",
                    required = true,
                    type = "file",
                });
                operation.consumes.Add("multipart/form-data");
            }

@glyons
Copy link

glyons commented Apr 10, 2017

nice one @razonrus

@AndrewBragdon
Copy link

It would be nicer to do this via attribute on the method, rather than in a central place

@heldersepu
Copy link
Contributor

heldersepu commented Jan 11, 2018

@AndrewBragdon I think one of the comments: #120 (comment) does exactly that

@xerikai
Copy link

xerikai commented Jan 12, 2018

I'm attempting to do what @glyons describes, but the code never seems to be executed when I simply add it as an attribute to the controller method.
This is what my controller method is looking like
[Route("PostDecryptionKey"), HttpPost, Shared.Models.ImportFileParamType.SwaggerForm("DecryptionKey","Upload Decryption Key")]
public async Task PostDecryptionKey([FromBody] string GroupID)

I ONLY created the attribute and decorated the controller with it, so if I was supposed to do something in the swaggerconfig, that may be what I'm missing.

When I refactored it to be closer to what domaindrivendev suggested (including the swaggerconfig modification) then it works perfectly, but then I have to keep hardcoding a dependency on the operationID which I'd prefer not to do that.

I appreciate anyone who can chime in, :)

heldersepu added a commit to heldersepu/Swagger-Net-Test that referenced this issue Jan 12, 2018
@heldersepu
Copy link
Contributor

@xerikai see my commit, that is exactly what @glyons describes + @razonrus suggestion.
the final product is:
http://swashbuckletest.azurewebsites.net/swagger/ui/index?filter=Png#/PngImage/PngImage_Post

@xerikai
Copy link

xerikai commented Jan 12, 2018

Perfect, based on glyons I had put my sealed class swaggerformattribute inside the importfileparamtype class, after I looked at your checkin, I moved it out and everything started working. Thank you for the effort!

@RenanCarlosPereira
Copy link

RenanCarlosPereira commented Mar 3, 2018

senhores boa noite
achei uma solução que pode ajudar, neste caso abaixo, não depende de nomes de controles ou de actions, basta chamar na swagger e usar o IFormFile normalmente

using Microsoft.AspNetCore.Http;
using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Linq;

public class FileUploadOperation : IOperationFilter
{
    public void Apply(Operation operation, OperationFilterContext context)
    {
        if (operation.Parameters == null) return;

        var result = from a in context.ApiDescription.ParameterDescriptions
                     join b in operation.Parameters.OfType<NonBodyParameter>()
                     on a.Name equals b?.Name
                     where a.ModelMetadata.ModelType == typeof(IFormFile)
                     select b;


        result.ToList().ForEach(x =>
        {
            x.In = "formData";
            x.Description = "Upload file.";
            x.Type = "file";
        });
    }
}

a chamada fica assim


services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new Info { Title = "My API V1", Version = "v1" });
                c.OperationFilter<FileUploadOperation>();
            });

@theBoringCoder
Copy link

@RenanCarlosPereira your solution doesn't work when the IFormFile is tagged with [FromForm], but I've yet to figure out why. My knowledge of Swashbuckle is quite small.

@romipetrelis
Copy link

romipetrelis commented Oct 30, 2018

This is a bit hacky, @theBoringCoder , but it seems to be having the desired result when using [FromForm]. YMMV

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

            // if you don't use [Consumes("multipart/form-data")] on your operations, you don't need this
            if (!operation.Consumes.Contains("multipart/form-data")) return;

            var param = context.ApiDescription.ParameterDescriptions.FirstOrDefault(x =>
                x.ModelMetadata.ModelType == typeof(IFormFile));
            
            var existing = operation.Parameters.FirstOrDefault(x =>
                x.Name.Equals(param?.Name, StringComparison.InvariantCultureIgnoreCase));

            var toAdd = new NonBodyParameter
            {
                In = "formData",
                Type = "file",
                Description = existing?.Description,
                Name = existing?.Name ?? "file",
                Required = existing?.Required ?? false
            };

            if (existing != null) operation.Parameters.Remove(existing);
            
            operation.Parameters.Add(toAdd);
        }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests