Swashbuckle 5 and multipart/form-data HelpPages

三世轮回 提交于 2021-01-26 04:00:49

问题


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

回答1:


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



回答2:


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:

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);



回答3:


Extending @bkwdesign very useful answer...

His/her code includes:

//other parameters omitted for brevity

You can actually pull all the parameter information (for the non-multi-part form parameters) from the parameters to the filter. Inside the check for supportsDesiredFilter, do the following:

if (operation.parameters.Count != apiDescription.ParameterDescriptions.Count)
{
    throw new ApplicationException("Inconsistencies in parameters count");
}
operation.consumes.Add("multipart/form-data");

var parametersList = new List<Parameter>(apiDescription.ParameterDescriptions.Count + 1);
for (var i = 0; i < apiDescription.ParameterDescriptions.Count; ++i)
{
    var schema = schemaRegistry.GetOrRegister(apiDescription.ParameterDescriptions[i].ParameterDescriptor.ParameterType);

    parametersList.Add(new Parameter
    {
        name = apiDescription.ParameterDescriptions[i].Name,
        @in = operation.parameters[i].@in,
        description = operation.parameters[i].description,
        required = !apiDescription.ParameterDescriptions[i].ParameterDescriptor.IsOptional,
        type = apiDescription.ParameterDescriptions[i].ParameterDescriptor.ParameterType.FullName,
        schema = schema,
    });
}

parametersList.Add(new Parameter
{
    name = "fileToUpload",
    @in = "formData",
    description = "File to upload.",
    required = true,
    type = "file"
});
operation.parameters = parametersList;

first it checks to make sure that the two arrays being passed in are consistent. Then it walks through the arrays to pull out the required info to put into the collection of Swashbuckle Parameters.

The hardest thing was to figure out that the types needed to be registered in the "schema" in order to have them show up in the Swagger UI. But, this works for me.

Everything else I did was consistent with @bkwdesign's post.



来源:https://stackoverflow.com/questions/39152612/swashbuckle-5-and-multipart-form-data-helppages

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!