How can I upload a list of files (images) and json data to ASP.NET Core Web API controller using multipart upload?
I can successfully receive a list of files, uploaded with multipart/form-data
content type like that:
public async Task<IActionResult> Upload(IList<IFormFile> files)
And of course I can successfully receive HTTP request body formatted to my object using default JSON formatter like that:
public void Post([FromBody]SomeObject value)
But how can I combine these two in a single controller action? How can I upload both images and JSON data and have them bind to my objects?
There is simpler solution, heavily inspired by Andrius' answer. By using
the ModelBinderAttribute
you don't have to specify a model or binder provider. This saves a lot of code. Your controller action would look like this:
public IActionResult Upload(
[ModelBinder(BinderType = typeof(JsonModelBinder))] SomeObject value,
IList<IFormFile> files)
{
// Use serialized json object 'value'
// Use uploaded 'files'
}
Code behind JsonModelBinder
(see GitHub or use NuGet package):
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
public class JsonModelBinder : IModelBinder {
public Task BindModelAsync(ModelBindingContext bindingContext) {
if (bindingContext == null) {
throw new ArgumentNullException(nameof(bindingContext));
}
// Check the value sent in
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult != ValueProviderResult.None) {
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
// Attempt to convert the input value
var valueAsString = valueProviderResult.FirstValue;
var result = Newtonsoft.Json.JsonConvert.DeserializeObject(valueAsString, bindingContext.ModelType);
if (result != null) {
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
return Task.CompletedTask;
}
}
Here is an example of a raw http request as accepted by the controller action Upload
above.
A multipart/form-data
request is split into multiple parts each separated by the specified boundary=12345
. Each part got a name assigned in its Content-Disposition
-header. With these names default ASP.Net-Core
knows which part is bound to which parameter in the controller action.
Files that are bound to IFormFile
additionally need to specify a filename
as in the second part of the request. Content-Type
is not required.
Another thing to note is that the json parts need to be deserializable into the parameter types as defined in the controller action. So in this case the type SomeObject
should have a property key
of type string
.
POST http://localhost:5000/home/upload HTTP/1.1
Host: localhost:5000
Content-Type: multipart/form-data; boundary=12345
Content-Length: 218
--12345
Content-Disposition: form-data; name="value"
{"key": "value"}
--12345
Content-Disposition: form-data; name="files"; filename="file.txt"
Content-Type: text/plain
This is a simple text file
--12345--
Postman can be used to call the action and test your server side code. This is quite simple and mostly UI driven. Create a new request and select form-data in the Body-Tab. Now you can choose between text and file for each part of the reqeust.