Adding a File Upload Field to Your Swagger UI With Swashbuckle

If you’re building ASP.NET Core Web APIs, then I hope you’ve heard of Swashbuckle – the tool to generate the Swagger UI automatically for all of your controllers to make manual testing your endpoints visual and simple.

Out of the box, the documentation helps you set up your UI, handle different ways to authenticate (which we will touch on in a later post), and have it all hooked up to your controllers. However, this only handles the most common cases of required requests with query string parameters and HTTP Body content.

In this post, we’ll look at a quick and easy way to also add File upload fields for your API endpoints that consume IFormFile properties to make testing file uploading even easier.

Basic Swagger Setup

First thing’s first, install that puppy:

Package Manager : Install-Package Swashbuckle.AspNetCore
CLI : dotnet add package Swashbuckle.AspNetCore

Let’s first look at a simple swagger setup as our baseline before we add everything for our HTTP Header Field.

Startup.cs

//...
public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddSwaggerGen(config =>
    {
        config.SwaggerDoc("v1", new Info { Title = "My API", Version = "V1" });
    });
    // ...
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    // ...
    app.UseMvc();

    app.UseSwagger();
    app.UseSwaggerUI(config =>
    {
        config.SwaggerEndpoint("/swagger/v1/swagger.json", "My API v1");
    });
    // ...
}
//...

This setup gives us all we need for our basic UI and wireup to our controllers!
Running this gives us our basic swagger at /swagger:
Swagger no header

Adding a File Upload Field

What we have to do now is add an OperationFilter to our swagger generation. These OperationFilters can do a whole lot and enable us to customize the swagger document created which is what drives the fields and info on the UI.

Let’s start by creating our FormFileSwaggerFilter class.

FormFileSwaggerFilter.cs

/// <summary>
/// Filter to enable handling file upload in swagger
/// </summary>
public class FormFileSwaggerFilter : IOperationFilter
{
    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"
            });
        }
    }
}

This operation filter takes the operation parameters, then uses reflection to find the type of the field. If the field is an IFormFile, then we add a new file field from the formData section to our parameters. This in turn will update our swagger definition json file, and when rendered adds the field to our UI.

This even works great with endpoints that take a separate HTTP Body, query parameters, and files!

Now we need to reference it in our Startup when we initialize swagger:

Startup.cs

//...
public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddSwaggerGen(config =>
    {
        config.SwaggerDoc("v1", new Info { Title = "My API", Version = "V1" });

        config.OperationFilter<FormFileSwaggerFilter>();
    });
    // ...
}

Here’s an example controller with an endpoint that uses the file upload:

FileUploadController

[Route("api/[controller]")]
public class FileUploadController : Controller
{
    [HttpPost]
    public async Task<IActionResult> CreateMediaItem(string name, [FromForm]IFormFile file)
    {
        // Do something with the file
        return Ok();
    }
}

This controller ends up rendering in our Swagger UI as:
Fileupload_swagger

And using this, we can now submit our request with an uploaded file and all our other parameters!


If you like what you see, don’t forget to follow me on twitter @Suave_Pirate, check out my GitHub, and subscribe to my blog to learn more mobile developer tips and tricks!

Interested in sponsoring developer content? Message @Suave_Pirate on twitter for details.

Advertisement

8 thoughts on “Adding a File Upload Field to Your Swagger UI With Swashbuckle”

  1. While the IOperation filter does correctly put a file input in the swagger UI, when I select a file and click execute the “file” variable is null when the controller action receives the request.

    Like

    1. What version of asp.net core are you using and what version of swashbuckle? I use this everyday and it works perfectly for me. Wondering if something else in your setup is missing or conflicting somehow.

      Like

  2. I experienced the same behavior with .net core 2.1. I experimented a bit with the controller and when I am using [FromForm]IFormFile file directly in the method signature the IFormFile is null. When I wrap the IFormFile in a class the IFormFile is not null:

    public class FileUploadDTO
    {
    public IFormFile FormFile { get; set; }
    }
    public IActionResult UploadImage([FromForm]FileUploadDTO file)

    Like

    1. Is showing the browse button after you wrapped the IFormeFile in a class?

      For me is not showing this way and when i put IFormFile directly on the controller the button is show but the paramater always null also.

      I’m using dotnet 2.1.4.

      Like

  3. Thanks for the awesome article.
    Just to note that this does not work if you have the DescribeAllParametersInCamelCase option set.
    I hade to remove case sensitivity to get it working:
    “`
    public class FormFileSwaggerFilter : IOperationFilter
    {
    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();
    var formFileSubParameterNames = new List();

    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;

    var matchesNestedFormFileProperty = formFileSubParameterNames.Any(p => parameter.Name.StartsWith(p + “.”, StringComparison.InvariantCultureIgnoreCase));
    var matchesFormFileDefaultProperty = formFilePropertyNames.Contains(parameter.Name, StringComparer.InvariantCultureIgnoreCase);
    if (matchesNestedFormFileProperty || matchesFormFileDefaultProperty)
    {
    parameters.Remove(parameter);
    }
    }

    foreach (var formFileParameter in formFileParameterNames)
    {
    parameters.Insert(0, new NonBodyParameter()
    {
    Name = formFileParameter,
    Type = “file”,
    In = “formData”
    });
    }
    }
    }
    “`

    Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s