File upload with React frontend and ASP.NET core backend

File upload with React frontend and ASP.NET core backend

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 request screenshot from Chrome Developers window
The request screenshot from Chrome Developers window

The quick watch screenshot from Visual Studio debugger window

The quick watch screenshot from Visual Studio debugger window
The quick watch screenshot from Visual Studio debugger window

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.