Handling file uploads in Swagger UI with ASP.NET Core
Image from Unsplash

Handling file uploads in Swagger UI with ASP.NET Core

Swagger UI file upload in ASP.NET Core WebApi

Swagger is the most convenient way to document your REST api endpoints. It provides generating of documentation on the fly based on the code structure, which shortens the time of creating the documentation drastically. This leaves more time for developer to actually focus on problem rather than on the actual documentation writing.

Most of the things in Swagger are put of the box meaning most of the work will be done just by adding references to the Swashbuckle NuGet packages and wiring them up in ASP.NET Core DI container and pipeline.

However, there are always things that require fine tuning. These are some of the requirements and functionalities you would not expect in every single project, but they are required decent amount of times to make you build a reusable piece of code. One of those things are file uploads. Swagger is good when it comes to plain text/json messages, but as soon as you need a mixed content to be sent against some endpoint, you have to dig a little bit into the code and do those fine tuning I mentioned before.

I believe there is no better way to explain something than doing it on an actual example, so for this article I will use a simple ASP.NET Core API controller which will expect a file upload. We are going to create a simple Swagger UI for it and test it.

Here is the bare controller without any documentation.

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using System.IO;
namespace FileUploadSample.Controllers
{
[Route("[controller]")]
public class UploadsController : ControllerBase
{
const String folderName = "files";
readonly String folderPath = Path.Combine(Directory.GetCurrentDirectory(), folderName);
public UploadsController()
{
if (!Directory.Exists(folderPath))
{
Directory.CreateDirectory(folderPath);
}
}
[HttpPost]
public async Task<IActionResult> Post(
            [FromForm(Name = "myFile")]IFormFile myFile)
        {
                using (var fileContentStream = new MemoryStream())
                {
                    await myFile.CopyToAsync(fileContentStream);
                    await System.IO.File.WriteAllBytesAsync(Path.Combine(folderPath, myFile.FileName), fileContentStream.ToArray());
                }
                return CreatedAtRoute(routeName: "myFile", routeValues: new { filename = myFile.FileName }, value: null); ;
        }

        [HttpGet("{filename}", Name = "myFile")]
        public async Task<IActionResult> Get([FromRoute] String filename)
        {
            var filePath = Path.Combine(folderPath, filename);
            if (System.IO.File.Exists(filePath))
            {
                return File(await System.IO.File.ReadAllBytesAsync(filePath), "application/octet-stream", filename);
            }
            return NotFound();
        }
    }
}
    

You can test this controller action for posting and getting the file using POSTMAN, just to make sure things are working and our desired functionality, which is uploading and getting the file from the WebAPI endpoints.

File Post

Following the REST convention we are returning 201 Created HTTP status code along with Location header value which points to the folder GET url. If you try to load value of Location header key in the browser, you will gte file download dialog even if you uploaded image. The reason for that is application/octet-stream content-type which forces your client to download the content regardless of the actual content type.

We have our intended functionality in place, we can now start documenting this simple controller using Swagger.

Before we start adding any code in the DI container and pipeline methods of Startup.cs, we need to add NuGet package references in the .csproj file

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netcoreapp2.2</TargetFramework>
    <AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.App" />
    <PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="2.2.3" />
    <!-- Swagger API documentation NuGet packages -->
    <PackageReference Include="Swashbuckle.AspNetCore" Version="4.0.1" />
    <PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="4.0.1" />
    <PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="4.0.1" />
  </ItemGroup>

</Project>

    

Now when we have Swashbuckle.Swagger NuGet packages included in the project, we can start modifying Startup.cs methods. For a quick start with Swagger UI in Web API ASP.NET Core project, you can us Microsoft official documentation page Get started with Swashbuckle and ASP.NET Core. For clarity reasons, I excluded API versioning from the Swagger documentation and hard-coded only one version. If you want to know more about the API versioning, you can check Advanced versioning in ASP.NET Core Web API article for more details on how to use API versioning.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace FileUploadSample
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSwaggerGen(
                options =>
                {
                    options.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info { Title = "My API", Version = "v1" });
                });

            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

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

            app.UseMvc();
        }
    }
}

    

Now, as soon as you fire up your WebApi project from Visual Studio, you will have Swagger up and running. Swagger UI is by default available from /Swagger path.

Swagger Default

At first look this UI looks just fine, but the problem starts when you expand POST endpoint UI. You probably did not expect this to be displayed. Fortunately nothing is wrong with the functionality. It is just that Swagger is drilling through the exposed type of parameter of Post method in UploadsController class.

Swagger Upload

Like this, your API will still work as expected, but for testing purposes, you still need to go back to POSTMAN and one of the goals of adding Swgger UI was to have everything in one place. What we can do to overcome this issue is to implement Swashbuckle.AspNetCore.SwaggerGen.IOperationFilter interface and add it to the Swagger options.

This filter will allow us to control Swagger UI behavior for the specific parameter of the endpoint methods in the controller.

using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Collections.Generic;

namespace FileUploadSample
{
    public class SwaggerFileOperationFilter : IOperationFilter
    {
        public void Apply(Operation operation, OperationFilterContext context)
        {
            if (operation.OperationId == "Post")
            {
                operation.Parameters = new List<IParameter>
                {
                    new NonBodyParameter
                    {
                        Name = "myFile",
                        Required = true,
                        Type = "file",
                        In = "formData"
                    }
                };
            }
        }
    }
}

    

Now just to add this filter to Swagger options in the DI container setup in Startup.cs

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSwaggerGen(
                options =>
                {
                    options.SwaggerDoc("v1", new Swashbuckle.AspNetCore.Swagger.Info { Title = "My API", Version = "v1" });

                    options.OperationFilter<SwaggerFileOperationFilter>();
                });

            services.AddMvc();
        }
    

The only thing left it is to check the UI representation in Swagger UI

Swagger Upload Ui

You can now use auto-generated Swagger UI to post the data and test the API endpoint without having to use POSTMAN or any external tool. This solution works just fine if we have a single method for uploading the file, but quite often, even when you start with single file upload, along the way there is a possibility that you will need other file upload endpoints. As you can see from the code, we only handle specific method name, so this solution is not that much re-usable.

We an use OperationFilterContext parameter to determine whether the parameter is Microsoft.AspNetCore.Http.IFormFile and apply our filter.

using Swashbuckle.AspNetCore.Swagger;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Collections.Generic;
using System.Linq;

namespace FileUploadSample
{
    public class SwaggerFileOperationFilter : IOperationFilter
    {
        public void Apply(Operation operation, OperationFilterContext context)
        {
            var fileParams = context.MethodInfo.GetParameters()
                .Where(p => p.ParameterType.FullName.Equals(typeof(Microsoft.AspNetCore.Http.IFormFile).FullName));

            if (fileParams.Any() && fileParams.Count() == 1)
            {
                operation.Parameters = new List<IParameter>
                {
                    new NonBodyParameter
                    {
                        Name = fileParams.First().Name,
                        Required = true,
                        Type = "file",
                        In = "formData"
                    }
                };
            }
        }
    }
}

    

However this piece of code only works in case you use endpoint explicitly for file upload as the list of parameters is cleared if the file parameter is present

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform includion ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article