using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc;
var path = $@"{_folder}\{fileName}"; var memoryStream = new MemoryStream(); using (var stream = new FileStream(path, FileMode.Open)) { await stream.CopyToAsync(memoryStream); } memoryStream.Seek(0, SeekOrigin.Begin);
透過 IFormFile 上傳檔案,是由 ASP.NET Core 控制緩衝記憶體,如果檔案太大或很頻繁耗用緩衝記憶體,容易使 ASP.NET Core 的緩衝記憶體到達上限,屆時就是它死給你看的時候了。 所以,如果系統會有上傳大檔的需求,又或者是會很頻繁的上傳檔案,強烈建議改用串流的方式,自己實作寫入硬碟位置,避免 ASP.NET Core 控制緩衝記憶體控制到溢位。
DisableFormValueModelBindingFilter
由於要自行處理 Request 來的資料,所以要把 原本的 Model Binding 移除 。 建立一個 Attribute 註冊在大型檔案上傳的 API,透過 Resource Filter 在 Model Binding 之前把它移除。
using System; using System.Globalization; using System.IO; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Net.Http.Headers;
namespaceMyWebsite.Helpers { publicstaticclassFileStreamingHelper { privatestaticreadonly FormOptions _defaultFormOptions = new FormOptions();
publicstaticasync Task<FormValueProvider> StreamFile(this HttpRequest request, Func<FileMultipartSection, Stream> createStream) { if (!MultipartRequestHelper.IsMultipartContentType(request.ContentType)) { thrownew Exception($"Expected a multipart request, but got {request.ContentType}"); }
// 把 request 中的 Form 依照 Key 及 Value 存到此物件 var formAccumulator = new KeyValueAccumulator();
var boundary = MultipartRequestHelper.GetBoundary( MediaTypeHeaderValue.Parse(request.ContentType), _defaultFormOptions.MultipartBoundaryLengthLimit); var reader = new MultipartReader(boundary, request.Body);
var section = await reader.ReadNextSectionAsync(); while (section != null) { // 把 Form 的欄位內容逐一取出 ContentDispositionHeaderValue contentDisposition; var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition);
if (hasContentDispositionHeader) { if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition)) { // 若此欄位是檔案,就寫入至 Stream; using (var targetStream = createStream(section.AsFileSection())) { await section.Body.CopyToAsync(targetStream); } } elseif (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition)) { // 若此欄位不是檔案,就把 Key 及 Value 取出,存入 formAccumulator var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name).Value; var encoding = GetEncoding(section); using (var streamReader = new StreamReader( section.Body, encoding, detectEncodingFromByteOrderMarks: true, bufferSize: 1024, leaveOpen: true)) { varvalue = await streamReader.ReadToEndAsync(); if (String.Equals(value, "undefined", StringComparison.OrdinalIgnoreCase)) { value = String.Empty; } formAccumulator.Append(key, value);
// 取得 Form 的下一個欄位 section = await reader.ReadNextSectionAsync(); }
// Bind form data to a model var formValueProvider = new FormValueProvider( BindingSource.Form, new FormCollection(formAccumulator.GetResults()), CultureInfo.CurrentCulture);
return formValueProvider; }
privatestatic Encoding GetEncoding(MultipartSection section) { MediaTypeHeaderValue mediaType; var hasMediaTypeHeader = MediaTypeHeaderValue.TryParse(section.ContentType, out mediaType); // UTF-7 is insecure and should not be honored. UTF-8 will succeed in // most cases. if (!hasMediaTypeHeader || Encoding.UTF7.Equals(mediaType.Encoding)) { return Encoding.UTF8; } return mediaType.Encoding; } } }
var model = new AlbumModel{ Title = formValueProvider.GetValue("title").ToString(), Date = Convert.ToDateTime(formValueProvider.GetValue("date").ToString()) };
// ...
return Ok(new { title = model.Title, date = model.Date.ToString("yyyy/MM/dd"), photoCount = photoCount }); }
DisableFormValueModelBindingFilter Action 套用此 Filter 後,HTML Form 就不會被轉換成物件傳入 Action,因此也就可以移除 Action 的參數了。
StreamFile StreamFile 會將 HTML Form 的內容以 FormValueProvider 包裝後回傳,並以委派方法讓你實做上傳的事件,以此例來說就是直接以串流的方式直接寫檔。 這樣就能避免 ASP.NET Core 依賴緩衝記憶體上傳檔案。