Asp.NET provide a binding technique to upload files IFormFile
. But that technique will consume memory as it buffers the content of the file in the memory or in the database.
To skip buffering and use streaming, we kind of have to code it in own our way.
Http Protocol and uploading file:
The simplest way to upload file is to create an Html file input
element, and include it in html form
with submit
button as follows:
1<form name="form1" method="post" enctype="multipart/form-data" action="api/upload">
2 <div>
3 <label for="caption">Image Caption</label>
4 <input name="caption" type="text" />
5 </div>
6 <div>
7 <label for="image1">Image File</label>
8 <input name="image1" type="file" />
9 </div>
10 <div>
11 <input type="submit" value="Submit" />
12 </div>
13</form>
We can describe the code as follows:
- the corner stone of uploading file is an input of type
file
, which the web browser will provide functionality to pick a file from local device. - when we upload file, we submit it as
Html Form
data, and the form should haveenctype
asmultipart/form-data
which is different from the usual form encoding:application/x-www-form-urlencoded
- the form’s action attribute point to the server side that will handle the uploading.
How the browser will send the data?
The browser will send the data as follows:
1POST http://localhost:50460/api/upload HTTP/1.1
2User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:12.0) Gecko/20100101 Firefox/12.0
3Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
4Accept-Language: en-us,en;q=0.5
5Accept-Encoding: gzip, deflate
6Content-Type: multipart/form-data; boundary=---------------------------41184676334
7Content-Length: 29278
8
9-----------------------------41184676334
10Content-Disposition: form-data; name="caption"
11
12any caption
13-----------------------------41184676334
14Content-Disposition: form-data; name="image1"; filename="GrandCanyon.jpg"
15Content-Type: image/jpeg
16
17(Binary data not shown)
18-----------------------------41184676334--
The above message has two three parts:
- the meta data that describe the http message. The important point to notice that we are defining the
boundary
that seperate these parts. - the second part (separated by the boundary) is the normal form encoded inputs (the caption)
- the last part is the file binary data encoded as
base64
.
It is the browser who does all that work of encoding the http message.
How to read it from the server side?
From the server side Asp.NET provide the IFromFile
to bind to the file
1[Route("upload")]
2public class UploadController : ApiController
3{
4
5[Route("")]
6[HttpPost]
7public async Task UploadFile([FromForm] IFormFile image1, [FromForm] string caption)
8{
9
10}
11}
Notice in the code above that the name of the parameter image
is the same name of the input
element on HTML form.
You can access the file with its content from the interface IFormFile
.
How Asp.NET upload the file:
Asp.NET will read the content of the mssage, and it will buffer it in memory, until it reach a specific size (64KB) specified in MemoryBufferThreshold setting.
if the size is bigger than MemoryBufferThreshold
, then the system will buffer it on the disk.
Upload file without HTML form and submit
If you are dealing with SPA application then you cannot use the built-in browser functionality to upload the file and you have to do it on JavaScript side.
The following code will do that part:
1async function uploadFile () {
2 let uploadedFile = document.getElementById('file').files[0];
3 const formData = new FormData();
4 formData.append("file", uploadedFile);
5 formData.append("caption", "some caption");
6
7 try {
8 const response = await fetch('api/upload', {
9 method: 'POST',
10 body: formData
11 });
12
13 if (response.ok) {
14 window.location.href = '/';
15 }
16
17 var result = 'Result: ' + response.status + ' ' +
18 response.statusText;
19 } catch (error) {
20 console.error('Error:', error);
21 }
22 }
Upload file using streaming
As we discussed before IFormFile
will do buffering for the file data.
What if we want to avoid that?
To reduce memory usage you can upload the file using streaming, but there is no off-the-shelf API for that in ASP.NET, and you have to go to lower level and rebuild the whole process of reading Http message yourself.
Let’s see some code
1 [HttpPost]
2 [Route(nameof(Upload))]
3 public async Task<IActionResult> Upload()
4 {
5 var request = HttpContext.Request;
6
7 // validation of Content-Type
8 // 1. first, it must be a form-data request
9 // 2. a boundary should be found in the Content-Type
10 if (!request.HasFormContentType ||
11 !MediaTypeHeaderValue.TryParse(request.ContentType, out var mediaTypeHeader) ||
12 string.IsNullOrEmpty(mediaTypeHeader.Boundary.Value))
13 {
14 return new UnsupportedMediaTypeResult();
15 }
16
17 var boundary = HeaderUtilities.RemoveQuotes(mediaTypeHeader.Boundary.Value).Value;
18 var reader = new MultipartReader(boundary, request.Body);
19 var section = await reader.ReadNextSectionAsync();
20
21 // This sample try to get the first file from request and save it
22 // Make changes according to your needs in actual use
23 while (section != null)
24 {
25 var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition,
26 out var contentDisposition);
27
28 if (hasContentDispositionHeader && contentDisposition.DispositionType.Equals("form-data") &&
29 !string.IsNullOrEmpty(contentDisposition.FileName.Value))
30 {
31 // Don't trust any file name, file extension, and file data from the request unless you trust them completely
32 // Otherwise, it is very likely to cause problems such as virus uploading, disk filling, etc
33 // In short, it is necessary to restrict and verify the upload
34 // Here, we just use the temporary folder and a random file name
35
36 // Get the temporary folder, and combine a random file name with it
37 var fileName = Path.GetRandomFileName();
38 var saveToPath = Path.Combine(Path.GetTempPath(), fileName);
39
40 using (var targetStream = System.IO.File.Create(saveToPath))
41 {
42 await section.Body.CopyToAsync(targetStream);
43 }
44
45 return Ok();
46 }
47
48 section = await reader.ReadNextSectionAsync();
49 }
50
51 // If the code runs to this location, it means that no files have been saved
52 return BadRequest("No files data in the request.");
53 }