.NET Core MVC Application File Upload To Physical Location With Streaming Technique(Useful For Large Files) - Part 2
AJAX Call To File Upload(MVC or Web API):
Let's develop an action method that displays a form with a file upload field.
Controller/AjaxFileUploadController.cs:
using Microsoft.AspNetCore.Mvc; namespace StreamFileUpload.App.Controllers { [Route("ajax-file-upload")] public class AjaxFileUplaodController : Controller { [Route("add-file")] public IActionResult AddFile() { return View(); } } }Let's develop the form that posts the data using the AJAX call.
Views/AjaxFileUpload/AddFile.cshtml:
<div> <form action="add-file" enctype="multipart/form-data" method="POST" onsubmit="AJAXSubmit(this);return false;"> <div class="form-group"> <label for="txtName">Name</label> <input type="text" class="form-control" id="txtName" name="Name"> </div> <div class="form-group"> <label for="txtAge">Age</label> <input type="text" class="form-control" id="txtAge" name="Age"> </div> <div class="form-group"> <label for="fileUpload">Upload Files</label> <input type="file" class="form-control-file" id="fileUpload" name="FileUpload"> </div> <button type="submit" class="btn btn-primary">Save</button> </form> </div>In the form 'action' attribute specified with the action route value of MVC post action method. 'onsubmit' is the form method executed on form submission. 'AJAXSubmit(this)' method is our javascript method which sends form data using ajax. 'return false;' is to stop the form to get reload.
Now implement 'AJAXSubmit()' method as follows.
Views/AjaxFileUpload/AddFile.cshtml:
<script> async function AJAXSubmit(formElement){ var formData = new FormData(formElement); try{ var response = await fetch(formElement.action,{ method: 'POST', body: formData }); if(response.status == 200){ alert("file uploaded success fully") }else{ alert("failed to upload file") } }catch(e){ } } </script>Reading form data and assigning to AJAX post body.
Let's add an MVC post action method as follows.
Controller/AjaxFileUploadController.cs:
using System; using System.Globalization; using System.IO; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Net.Http.Headers; using StreamFileUpload.App.Models; namespace StreamFileUpload.App.Controllers { [Route("ajax-file-upload")] public class AjaxFileUploadController : Controller { private readonly IWebHostEnvironment _webHostEnvironment; public AjaxFileUploadController(IWebHostEnvironment webHostEnvironment) { _webHostEnvironment = webHostEnvironment; } [Route("add-file")] [HttpPost] public async TaskHere post action method code explained in detail in Part 1.SaveFileToPhysicalFolder() { var boundary = HeaderUtilities.RemoveQuotes( MediaTypeHeaderValue.Parse(Request.ContentType).Boundary ).Value; var reader = new MultipartReader(boundary, Request.Body); var section = await reader.ReadNextSectionAsync(); var formAccumelator = new KeyValueAccumulator(); while (section != null) { var hasContentDisposition = ContentDispositionHeaderValue.TryParse( section.ContentDisposition, out var contentDisposition ); if (hasContentDisposition) { if (contentDisposition.DispositionType.Equals("form-data") && (!string.IsNullOrEmpty(contentDisposition.FileName.Value) || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value))) { string fileStoragePath = $"{_webHostEnvironment.WebRootPath}/images/"; string fileName = Path.GetRandomFileName() + ".jpg"; // uploaded files form fileds byte[] fileByteArray; using (var memoryStream = new MemoryStream()) { await section.Body.CopyToAsync(memoryStream); fileByteArray = memoryStream.ToArray(); } using (var fileStream = System.IO.File.Create(Path.Combine(fileStoragePath,fileName))) { await fileStream.WriteAsync(fileByteArray); } } else { var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name).Value; using(var streamReader = new StreamReader(section.Body, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks:true, bufferSize:1024, leaveOpen:true)){ var value = await streamReader.ReadToEndAsync(); if(string.Equals(value, "undefined",StringComparison.OrdinalIgnoreCase)){ value = string.Empty; } formAccumelator.Append(key, value); } } } section = await reader.ReadNextSectionAsync(); } var profile = new Profile(); var formValueProvidere = new FormValueProvider( BindingSource.Form, new FormCollection(formAccumelator.GetResults()), CultureInfo.CurrentCulture ); var bindindSuccessfully = await TryUpdateModelAsync(profile,"",formValueProvidere); if(ModelState.IsValid){ // write log to save profile data to database } return Content("Uploaded successfully"); } } }
Now run the application and check ajax call posting data as below.
Uploading Multiple Files:
Using the 'multiple' attribute on the file input element will support uploading multiple files.
Views/AjaxFileUpload/AddFile.cshtml:
<input type="file" class="form-control-file" id="fileUpload" name="FileUpload" multiple>You can test multiple file upload by updating the input element as above.
Why Can't We Use Model Binding Or Partial Model Binding In-Stream Upload?:
We discussed that in the streaming technique we read the file from the request object, so we can't use model binding for streaming technique.
But we mostly think like reading uploading fields from request object and then all other form fields like 'textbox', 'checkbox', 'select', etc can be used Model Binding so that we can get rid custom model binding which we discussed Part 1. That kind of technique won't possible, the reason behind will be explored in the coming steps.
Let's update the post action method to use model binding as below:
public async Task<IActionResult> SaveFileToPhysicalFolder(Profile profile) { var boundary = HeaderUtilities.RemoveQuotes( MediaTypeHeaderValue.Parse(Request.ContentType).Boundary ).Value; var reader = new MultipartReader(boundary, Request.Body); var section = await reader.ReadNextSectionAsync(); while (section != null) { var hasContentDisposition = ContentDispositionHeaderValue.TryParse( section.ContentDisposition, out var contentDisposition ); if (hasContentDisposition) { if (contentDisposition.DispositionType.Equals("form-data") && (!string.IsNullOrEmpty(contentDisposition.FileName.Value) || !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value))) { string fileStoragePath = $"{_webHostEnvironment.WebRootPath}/images/"; string fileName = Path.GetRandomFileName() + ".jpg"; // uploaded files form fileds byte[] fileByteArray; using (var memoryStream = new MemoryStream()) { await section.Body.CopyToAsync(memoryStream); fileByteArray = memoryStream.ToArray(); } using (var fileStream = System.IO.File.Create(Path.Combine(fileStoragePath,fileName))) { await fileStream.WriteAsync(fileByteArray); } } } section = await reader.ReadNextSectionAsync(); } return Content("Uploaded successfully"); }Here implemented model binding of model type 'Profile' and trying to fetch upload fields from the request object.
Now run the application on the debug mode and check form fields bind to model as below.
Further debugging we encounter an exception as below
Hint: Exception occured in code:-An exception occurs because model binding reading the form of body stream before the action method starts executing. On the reading the entire stream of data, it removed from the request body, but in code, we are trying to read the stream from the empty object which results in an error.var section = await reader.ReadNextSectionAsync();Exception Message: Unexpected end of stream, the content may have already read by another component.
This sample of implement explains we can not use the model binding for uploading forms.
Wrapping Up:
Hopefully, this article will help to understand the additional information like Ajax call file upload, why can't be model binding, etc using the streaming technique in a file upload. I love to have your feedback, suggestions, and better techniques in the comment section.
Comments
Post a Comment