I love Swagger (OpenAPI). It’s a really nice way to share your API definitions, and it is so easy to integrate it into ASP.Net Core applications, that there’s literally no excuse not to use it.
The only glitch in Swagger’s integration is the lack of IFormFile support. So, if you’d like to upload a file you typically write the following action:
[HttpPost()] public IActionResult UploadFile(IFormFile file)
And that’s how it’s gonna be displayed in Swagger-UI (the real problem is, of course, in wrong swagger.json definition). As you could see, there’s everything but an ability to upload a file (actually, Swagger exposed all properties of IFormFile, which is completely useless).
There’s a nice discussion on Stackoverflow regarding this subject, and a couple of answers that solve the problem either via hardcoding or putting an attribute on an action. That didn’t sound like a generic solution for me, so I went down and implemented an Operation Filter that fixes the issue without any additional work.
The integration is quite simple (as with any other Swagger filter)
services.AddSwaggerGen(c => c.OperationFilter<FileUploadOperation>());
And here’s the actual filter. It handles IFormFile action parameters, IFormFile within other classes and even arrays(or any IEnumerables) of IFormFile.
/// <summary> /// adds an ability to upload files via Swagger (and autogenerated js client) /// </summary> public class FileUploadOperation : IOperationFilter { public void Apply(Operation operation, OperationFilterContext context) { if (context.ApiDescription.HttpMethod != "POST") return; if (operation.Parameters == null) operation.Parameters = new List<IParameter>(); var isFormFileFound = false; //try to find IEnumerable<IFormFile> parameters or IFormFile nested in other classes foreach (var parameter in operation.Parameters) { if (parameter is NonBodyParameter nonBodyParameter) { var methodParameter = context.ApiDescription.ParameterDescriptions.FirstOrDefault(x => x.Name == parameter.Name); if (methodParameter != null) { if (typeof(IFormFile).IsAssignableFrom(methodParameter.Type)) { nonBodyParameter.Type = "file"; nonBodyParameter.In = "formData"; isFormFileFound = true; } else if (typeof(IEnumerable<IFormFile>).IsAssignableFrom(methodParameter.Type)) { nonBodyParameter.Items.Type = "file"; nonBodyParameter.In = "formData"; isFormFileFound = true; } } } } //try to find IFormFile parameters of method var formFileParameters = context.ApiDescription.ActionDescriptor.Parameters .Where(x => x.ParameterType == typeof(IFormFile)).ToList(); foreach (var apiParameterDescription in formFileParameters) { operation.Parameters.Add(new NonBodyParameter { Name = apiParameterDescription.Name, In = "formData", Description = "Upload File", Required = true, Type = "file" }); } if (formFileParameters.Any()) { foreach (var propertyInfo in typeof(IFormFile).GetProperties()) { var parametersWithTheSameName = operation.Parameters.Where(x => x.Name == propertyInfo.Name); operation.Parameters.RemoveRange(parametersWithTheSameName); } } if (isFormFileFound) operation.Consumes.Add("multipart/form-data"); } }