Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Swashbuckle 5 and multipart/form-data HelpPages

I am stuck trying to get Swashbuckle 5 to generate complete help pages for an ApiController with a Post request using multipart/form-data parameters. The help page for the action comes up in the browser, but there is not included information on the parameters passed in the form. I have created an operation filter and enabled it in SwaggerConfig, the web page that includes the URI parameters, return type and other info derived from XML comments shows in the browser help pages; however, nothing specified in the operation filter about the parameters is there, and the help page contains no information about the parameters.

I must be missing something. Are there any suggestion on what I may have missed?

Operation filter code:

public class AddFormDataUploadParamTypes : IOperationFilter
{
    public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)         { 
         if (operation.operationId == "Documents_Upload") 
         { 
            operation.consumes.Add("multipart/form-data");
            operation.parameters = new[]
            {
                new Parameter
                 {
                     name = "anotherid",
                     @in  = "formData",
                     description = "Optional identifier associated with the document.",
                     required = false,
                     type = "string",
                     format = "uuid"

                 },
                 new Parameter
                 {
                     name = "documentid",
                     @in  = "formData",
                     description = "The document identifier of the slot reserved for the document.",
                     required = false,
                     type = "string",
                     format = "uuid"
                 },
                 new Parameter
                 {
                     name = "documenttype",
                     @in  = "formData",
                     description = "Specifies the kind of document being uploaded. This is not a file name extension.",
                     required = true,
                     type = "string"
                 },
                 new Parameter
                 {
                     name = "emailfrom",
                     @in  = "formData",
                     description = "A optional email origination address used in association with the document if it is emailed to a receiver.",
                     required = false,
                     type = "string"
                 },
                new Parameter
                 {
                     name = "emailsubject",
                     @in  = "formData",
                     description = "An optional email subject line used in association with the document if it is emailed to a receiver.",
                     required = false,
                     type = "string"
                 },
                 new Parameter 
                 { 
                     name = "file", 
                     @in = "formData", 
                     description = "File to upload.",
                     required = true, 
                     type = "file" 
                 }
             }; 
         } 
     } 
}
like image 570
Bob Avatar asked Aug 25 '16 18:08

Bob


2 Answers

I presume you figured out what your problem was. I was able to use your posted code to make a perfect looking 'swagger ui' interface complete with the file [BROWSE...] input controls.

I only modified your code slightly so it is applied when it detects my preferred ValidateMimeMultipartContentFilter attribute stolen from Damien Bond. Thus, my slightly modified version of your class looks like this:

public class AddFormDataUploadParamTypes<T> : IOperationFilter
{
    public void Apply(Operation operation, SchemaRegistry schemaRegistry, ApiDescription apiDescription)
    {
        var actFilters = apiDescription.ActionDescriptor.GetFilterPipeline();
        var supportsDesiredFilter = actFilters.Select(f => f.Instance).OfType<T>().Any();

        if (supportsDesiredFilter)
        {
            operation.consumes.Add("multipart/form-data");
            operation.parameters = new[]
            {
             //other parameters omitted for brevity
             new Parameter
             {
                 name = "file",
                 @in = "formData",
                 description = "File to upload.",
                 required = true,
                 type = "file"
             }
         };
        }
    }
}

Here's my Swagger UI: enter image description here

FWIW:

My NuGets

<package id="Swashbuckle" version="5.5.3" targetFramework="net461" />
<package id="Swashbuckle.Core" version="5.5.3" targetFramework="net461" />

Swagger Config Example

public class SwaggerConfig
{
    public static void Register()
    {
        var thisAssembly = typeof(SwaggerConfig).Assembly;

        GlobalConfiguration.Configuration 
            .EnableSwagger(c =>
                {

                    c.Schemes(new[] { "https" });

                    // Use "SingleApiVersion" to describe a single version API. Swagger 2.0 includes an "Info" object to
                    // hold additional metadata for an API. Version and title are required but you can also provide
                    // additional fields by chaining methods off SingleApiVersion.
                    //
                    c.SingleApiVersion("v1", "MyCorp.WebApi.Tsl");


                    c.OperationFilter<MyCorp.Swashbuckle.AddFormDataUploadParamTypes<MyCorp.Attr.ValidateMimeMultipartContentFilter>>();

                })
            .EnableSwaggerUi(c =>
                {

                    // If your API supports ApiKey, you can override the default values.
                    // "apiKeyIn" can either be "query" or "header"                                                
                    //
                    //c.EnableApiKeySupport("apiKey", "header");
                });
    }


}

UPDATE March 2019


I don't have quick access to the original project above, but, here's an example API controller from a different project...

Controller signature:

    [ValidateMimeMultipartContentFilter]
    [SwaggerResponse(HttpStatusCode.OK, Description = "Returns JSON object filled with descriptive data about the image.")]
    [SwaggerResponse(HttpStatusCode.NotFound, Description = "No appropriate equipment record found for this endpoint")]
    [SwaggerResponse(HttpStatusCode.BadRequest, Description = "This request was fulfilled previously")]
    public async Task<IHttpActionResult> PostSignatureImage(Guid key)

You'll note that there's no actual parameter representing my file in the signature, you can see below that I just spin up a MultipartFormDataStreamProvider to suck out the incoming POST'd form data.

Controller Body:

        var signatureImage = await db.SignatureImages.Where(img => img.Id == key).FirstOrDefaultAsync();
        if (signatureImage == null)
        {
            return NotFound();
        }

        if (!signatureImage.IsOpenForCapture)
        {
            ModelState.AddModelError("CaptureDateTime", $"This equipment has already been signed once on {signatureImage.CaptureDateTime}");
        }

        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        string fileName = String.Empty;
        string ServerUploadFolder = System.Web.Hosting.HostingEnvironment.MapPath("~/App_Data/");

        DirectoryInfo di = new DirectoryInfo(ServerUploadFolder + key.ToString());

        if (di.Exists == true)
            ModelState.AddModelError("id", "It appears an upload for this item is either in progress or has already occurred.");
        else
            di.Create();

        var fullPathToFinalFile = String.Empty;
        var streamProvider = new MultipartFormDataStreamProvider(di.FullName);
        await Request.Content.ReadAsMultipartAsync(streamProvider);


        foreach (MultipartFileData fileData in streamProvider.FileData)
        {
            if (string.IsNullOrEmpty(fileData.Headers.ContentDisposition.FileName))
            {
                return StatusCode(HttpStatusCode.NotAcceptable);
            }
            fileName = cleanFileName(fileData.Headers.ContentDisposition.FileName);

            fullPathToFinalFile = Path.Combine(di.FullName, fileName);

            File.Move(fileData.LocalFileName, fullPathToFinalFile);

            signatureImage.Image = File.ReadAllBytes(fullPathToFinalFile);

            break;
        }

        signatureImage.FileName = streamProvider.FileData.Select(entry => cleanFileName(entry.Headers.ContentDisposition.FileName)).First();
        signatureImage.FileLength = signatureImage.Image.LongLength;
        signatureImage.IsOpenForCapture = false;
        signatureImage.CaptureDateTime = DateTimeOffset.Now;
        signatureImage.MimeType = streamProvider.FileData.Select(entry => entry.Headers.ContentType.MediaType).First();

        db.Entry(signatureImage).State = EntityState.Modified;

        try
        {
            await db.SaveChangesAsync();

            //cleanup...
            File.Delete(fullPathToFinalFile);
            di.Delete();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!SignatureImageExists(key))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        char[] placeHolderImg = paperClipIcon_svg.ToCharArray();
        signatureImage.Image = Convert.FromBase64CharArray(placeHolderImg, 0, placeHolderImg.Length);

        return Ok(signatureImage);
like image 148
bkwdesign Avatar answered Nov 06 '22 03:11

bkwdesign


With Swashbuckle v5.0.0-rc4 methods listed above do not work. But by reading OpenApi spec I have managed to implement a working solution for uploading a single file. Other parameters can be easily added:

    public class FileUploadOperationFilter : IOperationFilter
    {
        public void Apply(OpenApiOperation operation, OperationFilterContext context)
        {
            var isFileUploadOperation =
                context.MethodInfo.CustomAttributes.Any(a => a.AttributeType == typeof(YourMarkerAttribute));
            if (!isFileUploadOperation) return;

            var uploadFileMediaType = new OpenApiMediaType()
            {
                Schema = new OpenApiSchema()
                {
                    Type = "object",
                    Properties =
                    {
                        ["uploadedFile"] = new OpenApiSchema()
                        {
                            Description = "Upload File",
                            Type = "file",
                            Format = "binary"
                        }
                    },
                    Required = new HashSet<string>()
                    {
                        "uploadedFile"
                    }
                }
            };
            operation.RequestBody = new OpenApiRequestBody
            {
                Content =
                {
                    ["multipart/form-data"] = uploadFileMediaType
                }
            };
        }
    }
like image 13
Alex Eyhorn Avatar answered Nov 06 '22 05:11

Alex Eyhorn