Published on: 12 November 2020
Author: Ramesh Kanjinghat
Context
Recently, I was working on a project that has React front end and ASP.NET core backend, my requirement was to upload a file along with some other data. Uploading a file with additional data from a React app to ASP.NET backend is very easy. What made me stumble was submitting collection data while uploading a file. When submitting a request the data can be spread across multiple parts of the request like form fields, query parameters, headers, body etc.
In ASP.NET, when submitting data in multiple parts of a request the data from each source/part must be defined as a different method parameter. For instance if you submit a file as form field and version as query string then the action method looks somewhat like
1public async Task<IActionResult> UploadDocumentAsync([FromForm]IFormFile file, [FromQuery]string version)
Instead if you want to keep all the data in a single method parameter then the source must be [FromForm] and all the data must be submitted as form fields. So, the action method looks somewhat like
1public async Task<IActionResult> UploadDocumentAsync([FromForm]UploadFileRequest uploadFileRequest)
I prefer to keep all my data wrapped in a single parameter so, I took the second path.
ASP.NET action method
1[HttpPost] 2[Route("uploaddocument")] 3public async Task<IActionResult> UploadDocumentAsync([FromForm]UploadFileRequest uploadFileRequest) { 4 try 5 { 6 //Process the request 7 return new StatusCodeResult(StatusCodes.Status200OK); 8 }catch(Exception ex) 9 { 10 this._logger.LogError(ex, ex.Message); 11 return new StatusCodeResult(StatusCodes.Status500InternalServerError); 12 } 13}
Model class UploadFileRequest
1public class UploadFileRequest { 2 public string Version { get; set; } 3 public IFormFile File { get; set; } 4 public IEnumerable<Author> Authors { get; set; } 5}
Model class Author
1public class Author{ 2 public string? FirstName { get; set; } 3 public string? LastName { get; set; } 4}
To submit a form with form fields you have to create a FormData object and append all the data as key value pairs. In my case, appending file and version was straight forward. At line# 4 I am appending the version and at line#5 the file. The last property of the model is Authors which is a collection. To append collection you need to do some extra work. In the client code the variable authors represents the authors collection. To append authors to form data I had to iterate through the collection and each property of item in the collection as a individual form field. Each form field must have unique name so I have used the index of the item to make each filed unique. Below at lines numbered from 7 to 13 I am doing the same.
React code to submit post request
1const handleFormSubmit = (e: FormEvent<HTMLFormElement>) => { 2 e.preventDefault(); 3 const formData = new FormData(); 4 formData.append('version', version); 5 formData.append('file', selectedFile as Blob, selectedFile?.name); 6 7 let index = 0; 8 authors?.forEach(element => { 9 if (element) { 10 formData.append(`authors[${index}].firstName`, element.firstName || ''); 11 formData.append(`authors[${index}].lastName`, element.lastName || ''); 12 index++; 13 } 14 }); 15 16 axios({ 17 method: 'POST', 18 data: formData, 19 url: "api/document/uploaddocument", 20 headers: { 21 'Content-Type': 'multipart/form-data', 22 } 23 }) 24 .then(() => { 25 alert('Document added succesfully'); 26 }) 27 .catch((err) => { 28 const errorMessage = typeof err.response.data === 'string' ? err.response.data : err.response.data.title; 29 alert(errorMessage); 30 }); 31 32 }
The request screenshot from Chrome Developers window
At the bottom of the image you can see the data is submitted as Form Data and properties of each author has indexer in their key.
The quick watch screenshot from Visual Studio debugger window
A working sample of this code can be found at https://github.com/Dhrutara/blogs.dhrutara.com.blogs/tree/main/aspnet-react-file-upload-with-additional-form-data
Tidbit
ASP.NET core can bind parameters from different sources, i.e. different parts of the request. They are
- FromQuery: Query parameters are bound to the model
- FromRoute: Route data are bound to the model
- FromForm: Posted form fields are bound to the model
- FromBody: Data in request body bound to the model
- FromHeader: Header values are bound to the model
FromServices This is not to bind the data from the incoming request instead services registered with dependency injection container. It is another way to inject services.
More about ASP.NET model binding can be found at https://docs.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-3.1