ASP.NET Core文件上传IFormFile于Request.Body的羁绊
前言
????在上篇文章深入探究ASP.NET Core讀取Request.Body的正確方式[1]中我們探討了很多人在日常開(kāi)發(fā)中經(jīng)常遇到的也是最基礎(chǔ)的問(wèn)題,那就是關(guān)于Request.Body的讀取方式問(wèn)題,看是簡(jiǎn)單實(shí)則很容易用不好。筆者也是非常榮幸的得到了許多同學(xué)的點(diǎn)贊支持,心理也是非常的興奮。在此期間在技術(shù)交流群中,有一位同學(xué)看到了我的文章之后提出了一個(gè)疑問(wèn),說(shuō)關(guān)于ASP.NET Core文件上傳IFormFile和Request.Body之間存在什么樣的關(guān)系。由于筆者沒(méi)對(duì)這方面有過(guò)相關(guān)的探究,也沒(méi)敢做過(guò)多回答,怕誤導(dǎo)了那位同學(xué),因此私下自己研究了一番,故作此文,希望能幫助更多的同學(xué)解除心中的疑惑。
IFormFile的使用方式
考慮到可能有的同學(xué)對(duì)ASP.NET Core文件上傳操作可能不是特別的理解,接下來(lái)咱們通過(guò)幾個(gè)簡(jiǎn)單的操作,讓大家簡(jiǎn)單的熟悉一下。
簡(jiǎn)單使用演示
首先是最簡(jiǎn)單的單個(gè)文件上傳的方式
[HttpPost] public string UploadFile (IFormFile formFile) {return $"{formFile.FileName}--{formFile.Length}--{formFile.ContentDisposition}--{formFile.ContentType}"; }非常簡(jiǎn)單的操作,通過(guò)IFormFile實(shí)例直接獲取文件信息,這里需要注意模型綁定的名稱(chēng)一定要和提交的表單值的name保持一致,這樣才能正確的完成模型綁定。還有的時(shí)候我們是要通過(guò)一個(gè)接口完成一批文件上傳,這個(gè)時(shí)候我們可以使用下面的方式
[HttpPost] public IEnumerable<string> UploadFiles(List<IFormFile> formFiles) {return formFiles.Select(i => $"{i.FileName}--{ i.Length}-{ i.ContentDisposition}--{ i.ContentType}"); }直接將模型綁定的參數(shù)聲明為集合類(lèi)型即可,同時(shí)也需要注意模型綁定的名稱(chēng)和上傳文件form的name要保持一致。不過(guò)有的時(shí)候你可能連List這種集合類(lèi)型也不想寫(xiě),想通過(guò)一個(gè)類(lèi)就能得到上傳的文件集合,好在微軟夠貼心,給我們提供了另一個(gè)類(lèi),操作如下
[HttpPost] public IEnumerable<string> UploadFiles3(IFormFileCollection formFiles) {return formFiles.Select(i => $"{i.FileName}--{ i.Length}-{ i.ContentDisposition}--{ i.ContentType}"); }對(duì)微軟的代碼風(fēng)格有了解的同學(xué)看到名字就知道,IFormFileCollection其實(shí)也是對(duì)IFormFile集合的封裝。有時(shí)候你可能都不想使用IFormFile的相關(guān)模型綁定,可能是你怕記不住這個(gè)名字,那還有別的方式能操作上傳文件嗎?當(dāng)然有,可以直接在Request表單中獲取上傳文件信息
[HttpPost] public IEnumerable<string> UploadFiles2() {IFormFileCollection formFiles = Request.Form.Files;return formFiles.Select(i => $"{i.FileName}--{ i.Length}-{ i.ContentDisposition}--{ i.ContentType}"); }其實(shí)它的本質(zhì)也是獲取到IFormFileCollection,不過(guò)這種方式更加的靈活。首先是不需要模型綁定名稱(chēng)不一致的問(wèn)題,其次是只要有Request的地方就可以獲取到上傳的文件信息。
操作上傳內(nèi)容
如果你想保存上傳的文件,或者是直接讀取上傳的文件信息,IFormFile為我們提供兩種可以操作上傳文件內(nèi)容信息的方式
?一種是將上傳文件的Stream信息Copy到一個(gè)新的Stream中?另一種是直接通過(guò)OpenReadStream的方式直接獲取上傳文件的Stream信息
兩種操作方式大致如下
[HttpPost] public async Task<string> UploadFile (IFormFile formFile) {if (formFile.Length > 0){//1.使用CopyToAsync的方式using var stream = System.IO.File.Create("test.txt");await formFile.CopyToAsync(stream);//2.使用OpenReadStream的方式直接得到上傳文件的StreamStreamReader streamReader = new StreamReader(formFile.OpenReadStream());string content = streamReader.ReadToEnd();}return $"{formFile.FileName}--{formFile.Length}--{formFile.ContentDisposition}--{formFile.ContentType}"; }更改內(nèi)容大小限制
ASP.NET Core會(huì)對(duì)上傳文件的大小做出一定的限制,默認(rèn)限制大小約是2MB(以字節(jié)為單位)左右,如果超出這個(gè)限制,會(huì)直接拋出異常。如何加下來(lái)我們看一下如何修改上傳文件的大小限制通過(guò)ConfigureServices的方式直接配置FormOptions的MultipartBodyLengthLimit
public void ConfigureServices(IServiceCollection services) {services.Configure<FormOptions>(options =>{// 設(shè)置上傳大小限制256MBoptions.MultipartBodyLengthLimit = 268435456;}); }這里只是修改了對(duì)上傳文件主題大小的限制,熟悉ASP.NET Core的同學(xué)可能知道,默認(rèn)情況下Kestrel對(duì)Request的Body大小也有限制,這時(shí)候我們還需要對(duì)Kestrel的RequestBody大小進(jìn)行修改,操作如下所示
public static IHostBuilder CreateHostBuilder(string[] args) =>Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder =>{webBuilder.ConfigureKestrel((context, options) =>{//設(shè)置Body大小限制256MBoptions.Limits.MaxRequestBodySize = 268435456;});webBuilder.UseStartup<Startup>();});很多時(shí)候這兩處設(shè)置都需要配合著一起使用,才能達(dá)到效果,用的時(shí)候需要特別的留意一下。
源碼探究
上面我們大致演示了IFormFile的基礎(chǔ)操作,我們上面的演示大致劃分為兩類(lèi),一種是通過(guò)模型綁定的方式而這種方式包含了IFormFile、List<IFormFile>、IFormFileCollection三種方式 ,另一種是通過(guò)Request.Form.Files的方式,為了搞懂他們的關(guān)系,就必須從模型綁定下手。
始于模型綁定
首先我們找到關(guān)于操作FormFile相關(guān)操作模型綁定的地方在FormFileModelBinder類(lèi)的BindModelAsync方法[點(diǎn)擊查看源碼????[2]]我們看到了如下代碼,展示的代碼刪除了部分邏輯,提取的是涉及到我們要關(guān)注的流程性的操作
public async Task BindModelAsync(ModelBindingContext bindingContext) {//獲取要綁定的參數(shù)類(lèi)型var createFileCollection = bindingContext.ModelType == typeof(IFormFileCollection);//判斷模型綁定參數(shù)類(lèi)型是IFormFileCollection類(lèi)型或可兼容IFormFileCollection類(lèi)型//其中ModelBindingHelper.CanGetCompatibleCollection是用來(lái)判斷模型綁定參數(shù)是否可以兼容IFormFileCollectionif (!createFileCollection && !ModelBindingHelper.CanGetCompatibleCollection<IFormFile>(bindingContext)){return;}//判斷模型綁定參數(shù)是否是集合類(lèi)型ICollection<IFormFile> postedFiles;if (createFileCollection){postedFiles = new List<IFormFile>();}else{//不是集合類(lèi)型的的話,包裝成為集合類(lèi)型//其中ModelBindingHelper.GetCompatibleCollection是將模型綁定參數(shù)綁包裝成集合類(lèi)型postedFiles = ModelBindingHelper.GetCompatibleCollection<IFormFile>(bindingContext);}//獲取要模型綁定的參數(shù)名稱(chēng)var modelName = bindingContext.IsTopLevelObject? bindingContext.BinderModelName ?? bindingContext.FieldName: bindingContext.ModelName;//給postedFiles添加值,postedFiles將承載上傳的所有文件await GetFormFilesAsync(modelName, bindingContext, postedFiles);if (postedFiles.Count == 0 &&bindingContext.OriginalModelName != null &&!string.Equals(modelName, bindingContext.OriginalModelName, StringComparison.Ordinal) &&!modelName.StartsWith(bindingContext.OriginalModelName + "[", StringComparison.Ordinal) &&!modelName.StartsWith(bindingContext.OriginalModelName + ".", StringComparison.Ordinal)){modelName = ModelNames.CreatePropertyModelName(bindingContext.OriginalModelName, modelName);await GetFormFilesAsync(modelName, bindingContext, postedFiles);}object value;//如果模型參數(shù)為IFormFileif (bindingContext.ModelType == typeof(IFormFile)){//并未獲取上傳文件相關(guān)直接返回if (postedFiles.Count == 0){return;}//集合存在則獲取第一個(gè)value = postedFiles.First();}else{//如果模型參數(shù)不為IFormFileif (postedFiles.Count == 0 && !bindingContext.IsTopLevelObject){return;}var modelType = bindingContext.ModelType;//如果模型參數(shù)為IFormFile[]則直接將postedFiles轉(zhuǎn)換為IFormFile[]if (modelType == typeof(IFormFile[])){Debug.Assert(postedFiles is List<IFormFile>);value = ((List<IFormFile>)postedFiles).ToArray();}//如果模型參數(shù)為IFormFileCollection則直接使用postedFiles初始化FileCollectionelse if (modelType == typeof(IFormFileCollection)){Debug.Assert(postedFiles is List<IFormFile>);value = new FileCollection((List<IFormFile>)postedFiles);}//其他類(lèi)型則直接賦值else{value = postedFiles;}}bindingContext.Result = ModelBindingResult.Success(value); }上面的源碼中涉及到了ModelBindingHelper模型綁定幫助類(lèi)[點(diǎn)擊查看源碼????[3]]相關(guān)的方法,主要是封裝模型綁定公共的幫助類(lèi)。涉及到的我們需要的方法邏輯,上面?zhèn)渥⒁呀?jīng)說(shuō)明了,這里就不展示源碼了,因?yàn)樗鼘?duì)于我們的流程來(lái)說(shuō)并不核心。
上面我們看到了用于初始化綁定集合的核心操作是GetFormFilesAsync方法[點(diǎn)擊查看源碼????[4]]話不多說(shuō)我們來(lái)直接看下它的實(shí)現(xiàn)邏輯
private async Task GetFormFilesAsync(string modelName,ModelBindingContext bindingContext,ICollection<IFormFile> postedFiles) {//獲取Request實(shí)例var request = bindingContext.HttpContext.Request;if (request.HasFormContentType){//獲取Request.Formvar form = await request.ReadFormAsync();//遍歷Request.Form.Filesforeach (var file in form.Files){//FileName如果未空的話不進(jìn)行模型綁定if (file.Length == 0 && string.IsNullOrEmpty(file.FileName)){continue;}//FileName等于模型綁定名稱(chēng)的話則添加postedFilesif (file.Name.Equals(modelName, StringComparison.OrdinalIgnoreCase)){postedFiles.Add(file);}}}else{_logger.CannotBindToFilesCollectionDueToUnsupportedContentType(bindingContext);} }看到這里得到的思路就比較清晰了,由于源碼需要順著邏輯走,我們大致總結(jié)一下關(guān)于FormFile模型綁定相關(guān)
?為了統(tǒng)一處理方便,不管是上傳的是單個(gè)文件還是多個(gè)文件,都會(huì)被包裝成ICollection<IFormFile>集合類(lèi)型?ICollection<IFormFile>集合里的值就是來(lái)自于Request.Form.Files?可綁定的類(lèi)型IFormFile、List<IFormFile>、IFormFileCollection等都是由ICollection<IFormFile>里的數(shù)據(jù)初始化而來(lái)?如果模型參數(shù)類(lèi)型是IFormFile實(shí)例非集合類(lèi)型,那么會(huì)從ICollection<IFormFile>集合中獲取第一個(gè)?模型綁定的參數(shù)名稱(chēng)要和上傳的FileName保持一致,否則無(wú)法進(jìn)行模型綁定
RequestForm的Files來(lái)自何處
通過(guò)上面的模型綁定我們了解到了ICollection<IFormFile>的值來(lái)自Request.Form.Files而得到RequestForm的值是來(lái)自ReadFormAsync方法,那么我們就從這個(gè)方法入手看看RequestForm是如何被初始化的,這是一個(gè)擴(kuò)展方法來(lái)自于RequestFormReaderExtensions擴(kuò)展類(lèi)[點(diǎn)擊查看源碼????[5]]大致代碼如下
public static Task<IFormCollection> ReadFormAsync(this HttpRequest request, FormOptions options,CancellationToken cancellationToken = new CancellationToken()) {// 一堆判斷邏輯由此省略var features = request.HttpContext.Features;var formFeature = features.Get<IFormFeature>();//首次請(qǐng)求初始化沒(méi)有Form的時(shí)候初始化一個(gè)FormFeatureif (formFeature == null || formFeature.Form == null){features.Set<IFormFeature>(new FormFeature(request, options));}//調(diào)用了HttpRequest的ReadFormAsync方法return request.ReadFormAsync(cancellationToken); }沒(méi)啥可說(shuō)的直接找到HttpRequest的ReadFormAsync方法,我們?cè)谏掀恼铝私膺^(guò)HttpRequest抽象類(lèi)默認(rèn)的實(shí)現(xiàn)類(lèi)是DefaultHttpRequest,所以我們找到DefaultHttpRequest的ReadFormAsync方法[點(diǎn)擊查看源碼????[6]]看一下它的實(shí)現(xiàn)
public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken) {return FormFeature.ReadFormAsync(cancellationToken); }從代碼中可以看到ReadFormAsync方法的返回值值來(lái)自FormFeature的ReadFormAsync方法,找到FormFeature的定義
private IFormFeature FormFeature => _features.Fetch(ref _features.Cache.Form, this, _newFormFeature)!; //其中_newFormFeature的定義來(lái)自其中委托的r值就是DefaultHttpRequest實(shí)例 private readonly static Func<DefaultHttpRequest, IFormFeature> _newFormFeature = r => new FormFeature(r, r._context.FormOptions ?? FormOptions.Default);通過(guò)上面這段兩段代碼我們可以看到,無(wú)論怎么兜兜轉(zhuǎn)轉(zhuǎn),最后都來(lái)到了FormFeature這個(gè)類(lèi),而且實(shí)例化這個(gè)類(lèi)的時(shí)候接受的值都是來(lái)自于DefaultHttpRequest實(shí)例,其中還包含F(xiàn)ormOptions,看著有點(diǎn)眼熟,不錯(cuò)上面我們?cè)O(shè)置的上傳大小限制值的屬性MultipartBodyLengthLimit正是來(lái)自這里。所有最終的單子都落到了FormFeature類(lèi)的ReadFormAsync方法[點(diǎn)擊查看源碼????[7]]找到源碼大致如下所示
public Task<IFormCollection> ReadFormAsync() => ReadFormAsync(CancellationToken.None); public Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken) {if (_parsedFormTask == null){if (Form != null){_parsedFormTask = Task.FromResult(Form);}else{_parsedFormTask = InnerReadFormAsync(cancellationToken);}}return _parsedFormTask; }最終指向了InnerReadFormAsync這個(gè)方法,而這個(gè)方法正是初始化Form的所在,也就是說(shuō)涉及到Form的初始化相關(guān)操作就是在這里進(jìn)行的,因?yàn)檫@個(gè)方法的邏輯比較多所以我們只關(guān)注ContentType是multipart/form-data的邏輯,這里我們也就只保留這類(lèi)的相關(guān)邏輯省去了其他的邏輯,有需要了解的同學(xué)可以自行查看源碼[點(diǎn)擊查看源碼????[8]]
private async Task<IFormCollection> InnerReadFormAsync(CancellationToken cancellationToken) {FormFileCollection? files = null;using (cancellationToken.Register((state) => ((HttpContext)state!).Abort(), _request.HttpContext)){var contentType = ContentType;// 判斷ContentType為multipart/form-data的時(shí)候if (HasMultipartFormContentType(contentType)){var formAccumulator = new KeyValueAccumulator();//得到boundary數(shù)據(jù)//Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"var boundary = GetBoundary(contentType, _options.MultipartBoundaryLengthLimit);// 把針對(duì)文件上傳的部分封裝到MultipartReadervar multipartReader = new MultipartReader(boundary, _request.Body){//Header個(gè)數(shù)限制HeadersCountLimit = _options.MultipartHeadersCountLimit,//Header長(zhǎng)度限制HeadersLengthLimit = _options.MultipartHeadersLengthLimit,//Body長(zhǎng)度限制BodyLengthLimit = _options.MultipartBodyLengthLimit,};//獲取下一個(gè)可解析的節(jié)點(diǎn),可以理解為每一個(gè)要解析的上傳文件信息var p = await multipartReader.ReadNextSectionAsync(cancellationToken);//不為null說(shuō)明已從Body解析出的上傳文件信息while (p != null){// 在這里解析內(nèi)容配置并進(jìn)一步傳遞它以避免重新分析if (!ContentDispositionHeaderValue.TryParse(p.ContentDisposition, out var contentDisposition)){throw new InvalidDataException("");}if (contentDisposition.IsFileDisposition()){var fileSection = new FileMultipartSection(p, contentDisposition);// 如果尚未對(duì)整個(gè)正文執(zhí)行緩沖,則為文件啟用緩沖p.EnableRewind(_request.HttpContext.Response.RegisterForDispose,_options.MemoryBufferThreshold, _options.MultipartBodyLengthLimit);// 找到結(jié)尾await p.Body.DrainAsync(cancellationToken);var name = fileSection.Name;var fileName = fileSection.FileName;FormFile file;//判斷Body默認(rèn)的流是否被修改過(guò),比如開(kāi)啟緩沖就會(huì)修改//如果Body不是默認(rèn)流則直接服務(wù)Bodyif (p.BaseStreamOffset.HasValue){file = new FormFile(_request.Body, p.BaseStreamOffset.GetValueOrDefault(), p.Body.Length, name, fileName);}else{// 如果沒(méi)有被修改過(guò)則獲取MultipartReaderStream的實(shí)例file = new FormFile(p.Body, 0, p.Body.Length, name, fileName);}file.Headers = new HeaderDictionary(p.Headers);//如果解析出來(lái)了文件信息則初始化FormFileCollectionif (files == null){files = new FormFileCollection();}if (files.Count >= _options.ValueCountLimit){throw new InvalidDataException("");}files.Add(file);}else if (contentDisposition.IsFormDisposition()){var formDataSection = new FormMultipartSection(p, contentDisposition);var key = formDataSection.Name;var value = await formDataSection.GetValueAsync();formAccumulator.Append(key, value);if (formAccumulator.ValueCount > _options.ValueCountLimit){throw new InvalidDataException("");}}else{//沒(méi)解析出來(lái)類(lèi)型}p = await multipartReader.ReadNextSectionAsync(cancellationToken);}if (formAccumulator.HasValues){formFields = new FormCollection(formAccumulator.GetResults(), files);}}}// 如果可重置,則恢復(fù)讀取位置為0(因?yàn)锽ody被讀取到了尾部)if (_request.Body.CanSeek){_request.Body.Seek(0, SeekOrigin.Begin);}//通過(guò)files得到FormCollectionif (files != null){Form = new FormCollection(null, files);}return Form; }這部分源碼比較多,而且這還是精簡(jiǎn)過(guò)只剩下ContentType為multipart/form-data的內(nèi)容,不過(guò)從這里我們就可以看出來(lái)FormFile的實(shí)例確實(shí)是依靠Request的Body里。其核心就在MultipartReader類(lèi)的ReadNextSectionAsync方法返回的Section數(shù)據(jù)[點(diǎn)擊查看源碼????[9]]通過(guò)上面的循環(huán)可以看到它是循環(huán)讀取的,它通過(guò)解析Request信息持續(xù)的迭代MultipartSection信息,這種操作方式正是處理一次上傳存在多個(gè)文件的情況,具體操作如下所示
private readonly BufferedReadStream _stream; private readonly MultipartBoundary _boundary; private MultipartReaderStream _currentStream;public MultipartReader(string boundary, Stream stream, int bufferSize) {//stream即是傳遞下來(lái)的RequestBody_stream = new BufferedReadStream(stream, bufferSize);_boundary = new MultipartBoundary(boundary, false);//創(chuàng)建MultipartReaderStream實(shí)例_currentStream = new MultipartReaderStream(_stream, _boundary) { LengthLimit = HeadersLengthLimit }; }public async Task<MultipartSection?> ReadNextSectionAsync(CancellationToken cancellationToken = new CancellationToken()) {//清空上一個(gè)節(jié)點(diǎn)的信息await _currentStream.DrainAsync(cancellationToken);// 如果返回了空值表示為最后一個(gè)節(jié)點(diǎn)if (_currentStream.FinalBoundaryFound){// 清空最后一個(gè)節(jié)點(diǎn)的掛載數(shù)據(jù)await _stream.DrainAsync(HeadersLengthLimit, cancellationToken);return null;}//讀取header信息var headers = await ReadHeadersAsync(cancellationToken);_boundary.ExpectLeadingCrlf = true;//組裝MultipartReaderStream實(shí)例_currentStream = new MultipartReaderStream(_stream, _boundary) { LengthLimit = BodyLengthLimit };//判斷流是否是原始的HttpRequestStreamlong? baseStreamOffset = _stream.CanSeek ? (long?)_stream.Position : null;//通過(guò)上面信息構(gòu)造MultipartSection實(shí)例return new MultipartSection() { Headers = headers, Body = _currentStream, BaseStreamOffset = baseStreamOffset }; }這里可以看出傳遞下來(lái)的RequestBody被構(gòu)建出了MultipartReaderStream實(shí)例,即MultipartReaderStream包裝了RequestBody中的信息[點(diǎn)擊查看源碼????[10]]看名字也知道它也是實(shí)現(xiàn)了Stream抽象類(lèi)
internal sealed class MultipartReaderStream : Stream { }而且我們看到BodyLengthLimit正是傳遞給了它的LengthLimit屬性,而B(niǎo)odyLengthLimit正是設(shè)置限制上傳文件的大小的屬性,我們找到使用LengthLimit屬性的地方,代碼如下所示[點(diǎn)擊查看源碼????[11]]
private int UpdatePosition(int read) {//更新Stream的Position的值,即更新讀取位置_position += read;//繼續(xù)讀取if (_observedLength < _position){//保存已經(jīng)讀取了的位置_observedLength = _position;//如果讀取了位置大于LengthLimit則拋出異常if (LengthLimit.HasValue && _observedLength > LengthLimit.GetValueOrDefault()){throw new InvalidDataException($"Multipart body length limit {LengthLimit.GetValueOrDefault()} exceeded.");}}return read; }從這段代碼我們可以看出,正是此方法限制了讀取的Body大小,通過(guò)我們對(duì)Stream的了解,這個(gè)UpdatePosition方法也必然會(huì)在Stream的Read方法也即是此處的MultipartReaderStream的Read方法中調(diào)用[點(diǎn)擊查看源碼????[12]]這樣才能起到限制的作用,大致看一下Read方法的實(shí)現(xiàn)代碼
public override int Read(byte[] buffer, int offset, int count) {//如果已經(jīng)讀到了結(jié)尾則直接返回0if (_finished){return 0;}PositionInnerStream();var bufferedData = _innerStream.BufferedData;// 匹配boundary的讀取邊界int read;if (SubMatch(bufferedData, _boundary.BoundaryBytes, out var matchOffset, out var matchCount)){// 匹配到了可讀取的邊界讀取并返回if (matchOffset > bufferedData.Offset){read = _innerStream.Read(buffer, offset, Math.Min(count, matchOffset - bufferedData.Offset));//返回讀取的長(zhǎng)度正是調(diào)用的UpdatePositionreturn UpdatePosition(read);}var length = _boundary.BoundaryBytes.Length;Debug.Assert(matchCount == length);var boundary = _bytePool.Rent(length);read = _innerStream.Read(boundary, 0, length);_bytePool.Return(boundary);Debug.Assert(read == length);//讀取RequestBody信息var remainder = _innerStream.ReadLine(lengthLimit: 100);remainder = remainder.Trim();//說(shuō)明讀取到了boundary的結(jié)尾if (string.Equals("--", remainder, StringComparison.Ordinal)){FinalBoundaryFound = true;}Debug.Assert(FinalBoundaryFound || string.Equals(string.Empty, remainder, StringComparison.Ordinal), "Un-expected data found on the boundary line: " + remainder);_finished = true;//返回讀取的長(zhǎng)度0說(shuō)明讀到了結(jié)尾return 0;}read = _innerStream.Read(buffer, offset, Math.Min(count, bufferedData.Count));//這里同樣是UpdatePositionreturn UpdatePosition(read); }通過(guò)這里就可清楚的看到MultipartReaderStream的Read方法就是在解析讀取的RequestBody的FormData類(lèi)型的信息,解析成我們可以直接讀取或者直接保存成文件的的原始的文件信息,它還有一個(gè)異步讀取的ReadAsync方法其實(shí)現(xiàn)原理類(lèi)似,在這里咱們就不在展示源碼了。最后我們?cè)賮?lái)看一下MultipartSection類(lèi)的實(shí)現(xiàn)[點(diǎn)擊查看源碼????[13]]我們上面知道了MultipartReaderStream才是在RequestBody中解析到文件上傳信息的關(guān)鍵所在,因此MultipartSection也就是包裝了讀取好的文件信息,我們來(lái)看一下它的代碼實(shí)現(xiàn)
public class MultipartSection {/// <summary>/// 從header中得到的ContentType類(lèi)型/// </summary>public string? ContentType{get{if (Headers != null && Headers.TryGetValue(HeaderNames.ContentType, out var values)){return values;}return null;}}/// <summary>/// 從header中得到的ContentDisposition信息/// </summary>public string? ContentDisposition{get{if (Headers != null && Headers.TryGetValue(HeaderNames.ContentDisposition, out var values)){return values;}return null;}}/// <summary>/// 讀取到的Header信息/// </summary>public Dictionary<string, StringValues>? Headers { get; set; }/// <summary>/// 從RequestBody中解析到的Stream信息,即MultipartReaderStream或其他RequestBody實(shí)例/// </summary>public Stream Body { get; set; } = default!;/// <summary>/// 已經(jīng)被讀取過(guò)的Stream位置/// </summary>public long? BaseStreamOffset { get; set; } }不出所料,這個(gè)類(lèi)正是包裝了上面一堆針對(duì)HTTP請(qǐng)求信息中讀取到的關(guān)于上傳的文件信息,由于上面設(shè)計(jì)到了幾個(gè)類(lèi),而且設(shè)計(jì)到了一個(gè)大致的讀取流程,為了防止同學(xué)們看起來(lái)容易蒙圈,這里咱們大致總結(jié)一下這里的讀取流程。通過(guò)上面的代碼我們了解到了涉及到的幾個(gè)重要的類(lèi)MultipartReader、MultipartReaderStream、MultipartSection知道這幾個(gè)類(lèi)在做什么就能明白到底是怎么通過(guò)RequestBody解析到文件信息的。大致解釋一下這幾個(gè)類(lèi)在做些什么
?通過(guò)MultipartReader類(lèi)的ReadNextSectionAsync方法可以得到MultipartSection的實(shí)例?MultipartSection類(lèi)包含的就是解析出RequestBody里的文件相關(guān)的信息包裝起來(lái),MultipartSection的Body屬性的值正是MultipartReaderStream的實(shí)例。?MultipartReaderStream類(lèi)正是通過(guò)讀取RequestBody里的各種boundary信息轉(zhuǎn)換為原始的文件內(nèi)容的Stream信息?FormFile的CopyToAsync和OpenReadStream方法都是Stream操作,而操作的Stream是來(lái)自MultipartReaderStream實(shí)例
總結(jié)
????這次的分析差不多就到這里了, 本篇文章主要討論了ASP.NET Core文件上傳操作類(lèi)IFormFile與RequestBody的關(guān)系,即如果通過(guò)RequestBody得到IFormFile實(shí)例相關(guān),畢竟是源碼設(shè)計(jì)到的東西比較多也比較散亂,我們?cè)賮?lái)大致的總結(jié)一下
?無(wú)論在Action上對(duì)IFormFile、List<IFormFile>、IFormFileCollection等進(jìn)行模型綁定,其實(shí)都是來(lái)自模型綁定處理類(lèi)FormFileModelBinder,而這個(gè)類(lèi)正是根據(jù)Request.Form.File的處理來(lái)判斷如何進(jìn)行模型綁定的。?而Request.Form.File本身其實(shí)就是IFormFileCollection類(lèi)型的,它的值也正是來(lái)自對(duì)RequestBody的解析,也正是我們今天的結(jié)論File的值來(lái)自RequestBody。?從RequestBody解析到IFormFileCollection是一個(gè)過(guò)程,而IFormFileCollection實(shí)際上是IFormFile的集合類(lèi)型,從RequestBody解析出來(lái)的也是單個(gè)IFormFile類(lèi)型,通過(guò)不斷的迭代添加得到的IFormFileCollection集合。?而從RequestBody中解析出來(lái)上傳的文件到IFormFile涉及到了幾個(gè)核心類(lèi),即MultipartReader、MultipartReaderStream和MultipartSection。其中MultipartSection是通過(guò)MultipartReader的ReadNextSectionAsync方法得到的,里面包含了解析好的上傳文件相關(guān)信息。而MultipartSection正是包裝了MultipartReaderStream,而這個(gè)類(lèi)才是真正讀取RequestBody得到可讀取的文件原始Stream的關(guān)鍵所在。
到了這里本文的全部?jī)?nèi)容就差不多結(jié)束了,希望本文能給大家?guī)?lái)收獲。我覺(jué)得有時(shí)候看源碼能解決許多問(wèn)題和心中的疑惑,因?yàn)槲覀冏鳛槌绦騿T每天寫(xiě)的也就是代碼,所以沒(méi)有比程序員直接讀取代碼能更好的了解想了解的信息了。但是讀源碼也有一定的困難,畢竟是別人的代碼,思維存在一定的偏差,更何況是一些優(yōu)秀的框架,作者們的思維很可能比我們要高出很多,所以很多時(shí)候讀起來(lái)會(huì)非常的吃力,即便如此筆者也覺(jué)得讀源碼是了解框架得到框架信息的一種比較行之有效的方式。
References
[1]?深入探究ASP.NET Core讀取Request.Body的正確方式:?https://www.cnblogs.com/wucy/p/14699717.html
[2]?點(diǎn)擊查看源碼????:?https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Mvc/Mvc.Core/src/ModelBinding/Binders/FormFileModelBinder.cs#L38
[3]?點(diǎn)擊查看源碼????:?https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Mvc/Mvc.Core/src/ModelBinding/ModelBindingHelper.cs
[4]?點(diǎn)擊查看源碼????:?https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Mvc/Mvc.Core/src/ModelBinding/Binders/FormFileModelBinder.cs#L142
[5]?點(diǎn)擊查看源碼????:?https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Http/Http/src/RequestFormReaderExtensions.cs#L21
[6]?點(diǎn)擊查看源碼????:?https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Http/Http/src/Internal/DefaultHttpRequest.cs#L166
[7]?點(diǎn)擊查看源碼????:?https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Http/Http/src/Features/FormFeature.cs#L108
[8]?點(diǎn)擊查看源碼????:?https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Http/Http/src/Features/FormFeature.cs#L125
[9]?點(diǎn)擊查看源碼????:?https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Http/WebUtilities/src/MultipartReader.cs#L68:46
[10]?點(diǎn)擊查看源碼????:?https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Http/WebUtilities/src/MultipartReaderStream.cs
[11]?點(diǎn)擊查看源碼????:?https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Http/WebUtilities/src/MultipartReaderStream.cs#L148
[12]?點(diǎn)擊查看源碼????:?https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Http/WebUtilities/src/MultipartReaderStream.cs#L162
[13]?點(diǎn)擊查看源碼????:?https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Http/WebUtilities/src/MultipartSection.cs
總結(jié)
以上是生活随笔為你收集整理的ASP.NET Core文件上传IFormFile于Request.Body的羁绊的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: ML.NET Cookbook:(10)
- 下一篇: .NET 搭建简单的通知服务