ASP.NET Core文件上传IFormFile于Request.Body的羁绊

前言

    在上篇文章深入探究ASP.NET Core读取Request.Body的正确方式[1]中我们探讨了很多人在日常开发中经常遇到的也是最基础的问题,那就是关于Request.Body的读取方式问题,看是简单实则很容易用不好。笔者也是非常荣幸的得到了许多同学的点赞支持,心理也是非常的兴奋。在此期间在技术交流群中,有一位同学看到了我的文章之后提出了一个疑问,说关于ASP.NET Core文件上传IFormFile和Request.Body之间存在什么样的关系。由于笔者没对这方面有过相关的探究,也没敢做过多回答,怕误导了那位同学,因此私下自己研究了一番,故作此文,希望能帮助更多的同学解除心中的疑惑。

IFormFile的使用方式

考虑到可能有的同学对ASP.NET Core文件上传操作可能不是特别的理解,接下来咱们通过几个简单的操作,让大家简单的熟悉一下。

简单使用演示

首先是最简单的单个文件上传的方式

[HttpPost]
public string UploadFile (IFormFile formFile)
{return $"{formFile.FileName}--{formFile.Length}--{formFile.ContentDisposition}--{formFile.ContentType}";
}

非常简单的操作,通过IFormFile实例直接获取文件信息,这里需要注意模型绑定的名称一定要和提交的表单值的name保持一致,这样才能正确的完成模型绑定。还有的时候我们是要通过一个接口完成一批文件上传,这个时候我们可以使用下面的方式

[HttpPost]
public IEnumerable<string> UploadFiles(List<IFormFile> formFiles)
{return formFiles.Select(i => $"{i.FileName}--{ i.Length}-{ i.ContentDisposition}--{ i.ContentType}");
}

直接将模型绑定的参数声明为集合类型即可,同时也需要注意模型绑定的名称和上传文件form的name要保持一致。不过有的时候你可能连List这种集合类型也不想写,想通过一个类就能得到上传的文件集合,好在微软够贴心,给我们提供了另一个类,操作如下

[HttpPost]
public IEnumerable<string> UploadFiles3(IFormFileCollection formFiles)
{return formFiles.Select(i => $"{i.FileName}--{ i.Length}-{ i.ContentDisposition}--{ i.ContentType}");
}

对微软的代码风格有了解的同学看到名字就知道,IFormFileCollection其实也是对IFormFile集合的封装。有时候你可能都不想使用IFormFile的相关模型绑定,可能是你怕记不住这个名字,那还有别的方式能操作上传文件吗?当然有,可以直接在Request表单中获取上传文件信息

[HttpPost]
public IEnumerable<string> UploadFiles2()
{IFormFileCollection formFiles = Request.Form.Files;return formFiles.Select(i => $"{i.FileName}--{ i.Length}-{ i.ContentDisposition}--{ i.ContentType}");
}

其实它的本质也是获取到IFormFileCollection,不过这种方式更加的灵活。首先是不需要模型绑定名称不一致的问题,其次是只要有Request的地方就可以获取到上传的文件信息。

操作上传内容

如果你想保存上传的文件,或者是直接读取上传的文件信息,IFormFile为我们提供两种可以操作上传文件内容信息的方式

•一种是将上传文件的Stream信息Copy到一个新的Stream中•另一种是直接通过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}";
}

更改内容大小限制

ASP.NET Core会对上传文件的大小做出一定的限制,默认限制大小约是2MB(以字节为单位)左右,如果超出这个限制,会直接抛出异常。如何加下来我们看一下如何修改上传文件的大小限制通过ConfigureServices的方式直接配置FormOptions的MultipartBodyLengthLimit

public void ConfigureServices(IServiceCollection services)
{services.Configure<FormOptions>(options =>{// 设置上传大小限制256MBoptions.MultipartBodyLengthLimit = 268435456;});
}

这里只是修改了对上传文件主题大小的限制,熟悉ASP.NET Core的同学可能知道,默认情况下Kestrel对Request的Body大小也有限制,这时候我们还需要对Kestrel的RequestBody大小进行修改,操作如下所示

public static IHostBuilder CreateHostBuilder(string[] args) =>Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(webBuilder =>{webBuilder.ConfigureKestrel((context, options) =>{//设置Body大小限制256MBoptions.Limits.MaxRequestBodySize = 268435456;});webBuilder.UseStartup<Startup>();});

很多时候这两处设置都需要配合着一起使用,才能达到效果,用的时候需要特别的留意一下。

源码探究

上面我们大致演示了IFormFile的基础操作,我们上面的演示大致划分为两类,一种是通过模型绑定的方式而这种方式包含了IFormFileList<IFormFile>IFormFileCollection三种方式 ,另一种是通过Request.Form.Files的方式,为了搞懂他们的关系,就必须从模型绑定下手。

始于模型绑定

首先我们找到关于操作FormFile相关操作模型绑定的地方在FormFileModelBinder类的BindModelAsync方法[点击查看源码????[2]]我们看到了如下代码,展示的代码删除了部分逻辑,提取的是涉及到我们要关注的流程性的操作

public async Task BindModelAsync(ModelBindingContext bindingContext)
{//获取要绑定的参数类型var createFileCollection = bindingContext.ModelType == typeof(IFormFileCollection);//判断模型绑定参数类型是IFormFileCollection类型或可兼容IFormFileCollection类型//其中ModelBindingHelper.CanGetCompatibleCollection是用来判断模型绑定参数是否可以兼容IFormFileCollectionif (!createFileCollection && !ModelBindingHelper.CanGetCompatibleCollection<IFormFile>(bindingContext)){return;}//判断模型绑定参数是否是集合类型ICollection<IFormFile> postedFiles;if (createFileCollection){postedFiles = new List<IFormFile>();}else{//不是集合类型的的话,包装成为集合类型//其中ModelBindingHelper.GetCompatibleCollection是将模型绑定参数绑包装成集合类型postedFiles = ModelBindingHelper.GetCompatibleCollection<IFormFile>(bindingContext);}//获取要模型绑定的参数名称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;//如果模型参数为IFormFileif (bindingContext.ModelType == typeof(IFormFile)){//并未获取上传文件相关直接返回if (postedFiles.Count == 0){return;}//集合存在则获取第一个value = postedFiles.First();}else{//如果模型参数不为IFormFileif (postedFiles.Count == 0 && !bindingContext.IsTopLevelObject){return;}var modelType = bindingContext.ModelType;//如果模型参数为IFormFile[]则直接将postedFiles转换为IFormFile[]if (modelType == typeof(IFormFile[])){Debug.Assert(postedFiles is List<IFormFile>);value = ((List<IFormFile>)postedFiles).ToArray();}//如果模型参数为IFormFileCollection则直接使用postedFiles初始化FileCollectionelse if (modelType == typeof(IFormFileCollection)){Debug.Assert(postedFiles is List<IFormFile>);value = new FileCollection((List<IFormFile>)postedFiles);}//其他类型则直接赋值else{value = postedFiles;}}bindingContext.Result = ModelBindingResult.Success(value);
}

上面的源码中涉及到了ModelBindingHelper模型绑定帮助类[点击查看源码????[3]]相关的方法,主要是封装模型绑定公共的帮助类。涉及到的我们需要的方法逻辑,上面备注已经说明了,这里就不展示源码了,因为它对于我们的流程来说并不核心。

上面我们看到了用于初始化绑定集合的核心操作是GetFormFilesAsync方法[点击查看源码????[4]]话不多说我们来直接看下它的实现逻辑

private async Task GetFormFilesAsync(string modelName,ModelBindingContext bindingContext,ICollection<IFormFile> postedFiles)
{//获取Request实例var request = bindingContext.HttpContext.Request;if (request.HasFormContentType){//获取Request.Formvar form = await request.ReadFormAsync();//遍历Request.Form.Filesforeach (var file in form.Files){//FileName如果未空的话不进行模型绑定if (file.Length == 0 && string.IsNullOrEmpty(file.FileName)){continue;}//FileName等于模型绑定名称的话则添加postedFilesif (file.Name.Equals(modelName, StringComparison.OrdinalIgnoreCase)){postedFiles.Add(file);}}}else{_logger.CannotBindToFilesCollectionDueToUnsupportedContentType(bindingContext);}
}

看到这里得到的思路就比较清晰了,由于源码需要顺着逻辑走,我们大致总结一下关于FormFile模型绑定相关

•为了统一处理方便,不管是上传的是单个文件还是多个文件,都会被包装成ICollection<IFormFile>集合类型•ICollection<IFormFile>集合里的值就是来自于Request.Form.Files•可绑定的类型IFormFileList<IFormFile>IFormFileCollection等都是由ICollection<IFormFile>里的数据初始化而来•如果模型参数类型是IFormFile实例非集合类型,那么会从ICollection<IFormFile>集合中获取第一个•模型绑定的参数名称要和上传的FileName保持一致,否则无法进行模型绑定

RequestForm的Files来自何处

通过上面的模型绑定我们了解到了ICollection<IFormFile>的值来自Request.Form.Files而得到RequestForm的值是来自ReadFormAsync方法,那么我们就从这个方法入手看看RequestForm是如何被初始化的,这是一个扩展方法来自于RequestFormReaderExtensions扩展类[点击查看源码????[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>();//首次请求初始化没有Form的时候初始化一个FormFeatureif (formFeature == null || formFeature.Form == null){features.Set<IFormFeature>(new FormFeature(request, options));}//调用了HttpRequest的ReadFormAsync方法return request.ReadFormAsync(cancellationToken);
}

没啥可说的直接找到HttpRequest的ReadFormAsync方法,我们在上篇文章了解过HttpRequest抽象类默认的实现类是DefaultHttpRequest,所以我们找到DefaultHttpRequest的ReadFormAsync方法[点击查看源码????[6]]看一下它的实现

public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken)
{return FormFeature.ReadFormAsync(cancellationToken);
}

从代码中可以看到ReadFormAsync方法的返回值值来自FormFeature的ReadFormAsync方法,找到FormFeature的定义

private IFormFeature FormFeature => _features.Fetch(ref _features.Cache.Form, this, _newFormFeature)!;
//其中_newFormFeature的定义来自其中委托的r值就是DefaultHttpRequest实例
private readonly static Func<DefaultHttpRequest, IFormFeature> _newFormFeature = r => new FormFeature(r, r._context.FormOptions ?? FormOptions.Default);

通过上面这段两段代码我们可以看到,无论怎么兜兜转转,最后都来到了FormFeature这个类,而且实例化这个类的时候接受的值都是来自于DefaultHttpRequest实例,其中还包含FormOptions,看着有点眼熟,不错上面我们设置的上传大小限制值的属性MultipartBodyLengthLimit正是来自这里。所有最终的单子都落到了FormFeature类的ReadFormAsync方法[点击查看源码????[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这个方法,而这个方法正是初始化Form的所在,也就是说涉及到Form的初始化相关操作就是在这里进行的,因为这个方法的逻辑比较多所以我们只关注ContentType是multipart/form-data的逻辑,这里我们也就只保留这类的相关逻辑省去了其他的逻辑,有需要了解的同学可以自行查看源码[点击查看源码????[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的时候if (HasMultipartFormContentType(contentType)){var formAccumulator = new KeyValueAccumulator();//得到boundary数据//Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"var boundary = GetBoundary(contentType, _options.MultipartBoundaryLengthLimit);// 把针对文件上传的部分封装到MultipartReadervar multipartReader = new MultipartReader(boundary, _request.Body){//Header个数限制HeadersCountLimit = _options.MultipartHeadersCountLimit,//Header长度限制HeadersLengthLimit = _options.MultipartHeadersLengthLimit,//Body长度限制BodyLengthLimit = _options.MultipartBodyLengthLimit,};//获取下一个可解析的节点,可以理解为每一个要解析的上传文件信息var p = await multipartReader.ReadNextSectionAsync(cancellationToken);//不为null说明已从Body解析出的上传文件信息while (p != null){// 在这里解析内容配置并进一步传递它以避免重新分析if (!ContentDispositionHeaderValue.TryParse(p.ContentDisposition, out var contentDisposition)){throw new InvalidDataException("");}if (contentDisposition.IsFileDisposition()){var fileSection = new FileMultipartSection(p, contentDisposition);// 如果尚未对整个正文执行缓冲,则为文件启用缓冲p.EnableRewind(_request.HttpContext.Response.RegisterForDispose,_options.MemoryBufferThreshold, _options.MultipartBodyLengthLimit);// 找到结尾await p.Body.DrainAsync(cancellationToken);var name = fileSection.Name;var fileName = fileSection.FileName;FormFile file;//判断Body默认的流是否被修改过,比如开启缓冲就会修改//如果Body不是默认流则直接服务Bodyif (p.BaseStreamOffset.HasValue){file = new FormFile(_request.Body, p.BaseStreamOffset.GetValueOrDefault(), p.Body.Length, name, fileName);}else{// 如果没有被修改过则获取MultipartReaderStream的实例file = new FormFile(p.Body, 0, p.Body.Length, name, fileName);}file.Headers = new HeaderDictionary(p.Headers);//如果解析出来了文件信息则初始化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{//没解析出来类型}p = await multipartReader.ReadNextSectionAsync(cancellationToken);}if (formAccumulator.HasValues){formFields = new FormCollection(formAccumulator.GetResults(), files);}}}// 如果可重置,则恢复读取位置为0(因为Body被读取到了尾部)if (_request.Body.CanSeek){_request.Body.Seek(0, SeekOrigin.Begin);}//通过files得到FormCollectionif (files != null){Form = new FormCollection(null, files);}return Form;
}

这部分源码比较多,而且这还是精简过只剩下ContentTypemultipart/form-data的内容,不过从这里我们就可以看出来FormFile的实例确实是依靠Request的Body里。其核心就在MultipartReader类的ReadNextSectionAsync方法返回的Section数据[点击查看源码????[9]]通过上面的循环可以看到它是循环读取的,它通过解析Request信息持续的迭代MultipartSection信息,这种操作方式正是处理一次上传存在多个文件的情况,具体操作如下所示

private readonly BufferedReadStream _stream;
private readonly MultipartBoundary _boundary;
private MultipartReaderStream _currentStream;public MultipartReader(string boundary, Stream stream, int bufferSize)
{//stream即是传递下来的RequestBody_stream = new BufferedReadStream(stream, bufferSize);_boundary = new MultipartBoundary(boundary, false);//创建MultipartReaderStream实例_currentStream = new MultipartReaderStream(_stream, _boundary) { LengthLimit = HeadersLengthLimit };
}public async Task<MultipartSection?> ReadNextSectionAsync(CancellationToken cancellationToken = new CancellationToken())
{//清空上一个节点的信息await _currentStream.DrainAsync(cancellationToken);// 如果返回了空值表示为最后一个节点if (_currentStream.FinalBoundaryFound){// 清空最后一个节点的挂载数据await _stream.DrainAsync(HeadersLengthLimit, cancellationToken);return null;}//读取header信息var headers = await ReadHeadersAsync(cancellationToken);_boundary.ExpectLeadingCrlf = true;//组装MultipartReaderStream实例_currentStream = new MultipartReaderStream(_stream, _boundary) { LengthLimit = BodyLengthLimit };//判断流是否是原始的HttpRequestStreamlong? baseStreamOffset = _stream.CanSeek ? (long?)_stream.Position : null;//通过上面信息构造MultipartSection实例return new MultipartSection() { Headers = headers, Body = _currentStream, BaseStreamOffset = baseStreamOffset };
}

这里可以看出传递下来的RequestBody被构建出了MultipartReaderStream实例,即MultipartReaderStream包装了RequestBody中的信息[点击查看源码????[10]]看名字也知道它也是实现了Stream抽象类

internal sealed class MultipartReaderStream : Stream
{
}

而且我们看到BodyLengthLimit正是传递给了它的LengthLimit属性,而BodyLengthLimit正是设置限制上传文件的大小的属性,我们找到使用LengthLimit属性的地方,代码如下所示[点击查看源码????[11]]

private int UpdatePosition(int read)
{//更新Stream的Position的值,即更新读取位置_position += read;//继续读取if (_observedLength < _position){//保存已经读取了的位置_observedLength = _position;//如果读取了位置大于LengthLimit则抛出异常if (LengthLimit.HasValue && _observedLength > LengthLimit.GetValueOrDefault()){throw new InvalidDataException($"Multipart body length limit {LengthLimit.GetValueOrDefault()} exceeded.");}}return read;
}

从这段代码我们可以看出,正是此方法限制了读取的Body大小,通过我们对Stream的了解,这个UpdatePosition方法也必然会在Stream的Read方法也即是此处的MultipartReaderStream的Read方法中调用[点击查看源码????[12]]这样才能起到限制的作用,大致看一下Read方法的实现代码

public override int Read(byte[] buffer, int offset, int count)
{//如果已经读到了结尾则直接返回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));//返回读取的长度正是调用的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();//说明读取到了boundary的结尾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;//返回读取的长度0说明读到了结尾return 0;}read = _innerStream.Read(buffer, offset, Math.Min(count, bufferedData.Count));//这里同样是UpdatePositionreturn UpdatePosition(read);
}

通过这里就可清楚的看到MultipartReaderStream的Read方法就是在解析读取的RequestBody的FormData类型的信息,解析成我们可以直接读取或者直接保存成文件的的原始的文件信息,它还有一个异步读取的ReadAsync方法其实现原理类似,在这里咱们就不在展示源码了。最后我们再来看一下MultipartSection类的实现[点击查看源码????[13]]我们上面知道了MultipartReaderStream才是在RequestBody中解析到文件上传信息的关键所在,因此MultipartSection也就是包装了读取好的文件信息,我们来看一下它的代码实现

public class MultipartSection
{/// <summary>/// 从header中得到的ContentType类型/// </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实例/// </summary>public Stream Body { get; set; } = default!;/// <summary>/// 已经被读取过的Stream位置/// </summary>public long? BaseStreamOffset { get; set; }
}

不出所料,这个类正是包装了上面一堆针对HTTP请求信息中读取到的关于上传的文件信息,由于上面设计到了几个类,而且设计到了一个大致的读取流程,为了防止同学们看起来容易蒙圈,这里咱们大致总结一下这里的读取流程。通过上面的代码我们了解到了涉及到的几个重要的类MultipartReaderMultipartReaderStreamMultipartSection知道这几个类在做什么就能明白到底是怎么通过RequestBody解析到文件信息的。大致解释一下这几个类在做些什么

•通过MultipartReader类的ReadNextSectionAsync方法可以得到MultipartSection的实例•MultipartSection类包含的就是解析出RequestBody里的文件相关的信息包装起来,MultipartSection的Body属性的值正是MultipartReaderStream的实例。•MultipartReaderStream类正是通过读取RequestBody里的各种boundary信息转换为原始的文件内容的Stream信息•FormFileCopyToAsyncOpenReadStream方法都是Stream操作,而操作的Stream是来自MultipartReaderStream实例

总结

    这次的分析差不多就到这里了, 本篇文章主要讨论了ASP.NET Core文件上传操作类IFormFile与RequestBody的关系,即如果通过RequestBody得到IFormFile实例相关,毕竟是源码设计到的东西比较多也比较散乱,我们再来大致的总结一下

•无论在Action上对IFormFileList<IFormFile>IFormFileCollection等进行模型绑定,其实都是来自模型绑定处理类FormFileModelBinder,而这个类正是根据Request.Form.File的处理来判断如何进行模型绑定的。•而Request.Form.File本身其实就是IFormFileCollection类型的,它的值也正是来自对RequestBody的解析,也正是我们今天的结论File的值来自RequestBody。•从RequestBody解析到IFormFileCollection是一个过程,而IFormFileCollection实际上是IFormFile的集合类型,从RequestBody解析出来的也是单个IFormFile类型,通过不断的迭代添加得到的IFormFileCollection集合。•而从RequestBody中解析出来上传的文件到IFormFile涉及到了几个核心类,即MultipartReaderMultipartReaderStreamMultipartSection。其中MultipartSection是通过MultipartReader的ReadNextSectionAsync方法得到的,里面包含了解析好的上传文件相关信息。而MultipartSection正是包装了MultipartReaderStream,而这个类才是真正读取RequestBody得到可读取的文件原始Stream的关键所在。

到了这里本文的全部内容就差不多结束了,希望本文能给大家带来收获。我觉得有时候看源码能解决许多问题和心中的疑惑,因为我们作为程序员每天写的也就是代码,所以没有比程序员直接读取代码能更好的了解想了解的信息了。但是读源码也有一定的困难,毕竟是别人的代码,思维存在一定的偏差,更何况是一些优秀的框架,作者们的思维很可能比我们要高出很多,所以很多时候读起来会非常的吃力,即便如此笔者也觉得读源码是了解框架得到框架信息的一种比较行之有效的方式。

References

[1] 深入探究ASP.NET Core读取Request.Body的正确方式: https://www.cnblogs.com/wucy/p/14699717.html
[2] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Mvc/Mvc.Core/src/ModelBinding/Binders/FormFileModelBinder.cs#L38
[3] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Mvc/Mvc.Core/src/ModelBinding/ModelBindingHelper.cs
[4] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Mvc/Mvc.Core/src/ModelBinding/Binders/FormFileModelBinder.cs#L142
[5] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Http/Http/src/RequestFormReaderExtensions.cs#L21
[6] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Http/Http/src/Internal/DefaultHttpRequest.cs#L166
[7] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Http/Http/src/Features/FormFeature.cs#L108
[8] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Http/Http/src/Features/FormFeature.cs#L125
[9] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Http/WebUtilities/src/MultipartReader.cs#L68:46
[10] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Http/WebUtilities/src/MultipartReaderStream.cs
[11] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Http/WebUtilities/src/MultipartReaderStream.cs#L148
[12] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Http/WebUtilities/src/MultipartReaderStream.cs#L162
[13] 点击查看源码????: https://github.com/dotnet/aspnetcore/blob/v5.0.6/src/Http/WebUtilities/src/MultipartSection.cs

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/302032.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

librosa能量_语音MFCC提取:librosa amp;amp; python_speech_feature(2019.12)

最近在阅读语音方向的论文&#xff0c;其中有个被提及很多的语音信号特征MFCC(Mel-Frequency Cepstral Coefficients)&#xff0c;找到了基于python的语音库librosa(version0.7.1)和python_speech_features(version0.6)&#xff0c;下文对这两个库计算MFCC的流程细节稍作梳理。…

线性代数与矩阵论 习题 1.2.2

试利用辗转相除法&#xff0c;求有理系数多项式$u(x)$和$v(x)$,使得$u(x)f(x)v(x)g(x)(f(x),g(x))$. (1)$f(x)3x^3-2x^2x2$,$g(x)x^2-x1$. 解:\begin{align*} 3x^3-2x^2x2&3x(x^2-x1)(x^2-2x2)\\x^2-x1&(x^2-2x2)(x-1)\\x^2-2x2&x(x-1)-(x-2)\\x-1&x-21\\\end{…

Uno 平台 一 WinUI终极跨平台方案(一)

以下是 Uno 平台的官方介绍&#xff1a;关于 Uno 平台Uno平台能够创建像素级完美的&#xff0c;只通过C#XAML编写的应用程序&#xff0c;能够跨平台运行在Windows&#xff0c;iOS&#xff0c;安卓&#xff0c;macOS&#xff0c;Linux和Web上&#xff0c;Uno 平台是免费和开源的…

Python程序员的30个常见错误

全世界只有3.14 % 的人关注了数据与算法之美在这篇文章中&#xff0c;我将总结新老Python程序员常犯的一些错误&#xff0c;以帮助你们在自己的工作避免犯同样或类似错误。推荐阅读《Python3.0科学计算指南》首先我要说明一下的是&#xff0c;这些都是来源于第一手的经验。我以…

米兰大学计算机科学,米兰大学

很多去意大利留学的学生会选择去米兰大学留学&#xff0c;那么留学米兰大学需要了解哪些重要事项呢&#xff1f;跟着出国留学网来看看吧!欢迎阅读。意大利留学米兰大学解析院校基本信息欧洲排名7所在省州伦巴第大区所在城市Milano学校性质公立建校年代1923年学校人数60406人官方…

650c公路车推荐_2020最具性价比的中高端公路整车盘点

当你从小白成为进阶玩家后&#xff0c;发现陪伴自己多年公路车逐渐不能满足自己需求时&#xff0c;你渐渐将目光转向高端公路车&#xff0c;当你被“坑蒙拐骗”试骑了顶级公路车后&#xff0c;换车的想法在脑中油然而生。然而理想很丰满&#xff0c;现实很骨感。干瘪腰包把你从…

Java程序员从笨鸟到菜鸟之(一百零四)java操作office和pdf文件(二)利用POI实现数据导出excel报表...

在上一篇博客中&#xff0c;我们简单介绍了java读取word&#xff0c;excel和pdf文档内容 &#xff0c;但在实际开发中&#xff0c;我们用到最多的是把数据库中数据导出excel报表形式。不仅仅简单的读取office中的数据.尤其是在生产管理或者财务系统中用的非常普遍&#xff0c;因…

为什么 HTTP3.0 使用 UDP 协议?

还记得以前我提过的常见面试题么&#xff1a;从浏览器地址栏输入网址&#xff0c;到网页彻底打开&#xff0c;中间都发生了什么&#xff1f;从浏览器输入网址&#xff0c;到网页打开&#xff0c;发生了什么&#xff0c;这题有多经典&#xff0c;很多业内技术大牛说用过这题面试…

程序员为啥365天都背电脑包?这答案我服!

全世界只有3.14 % 的人关注了数据与算法之美最近微博上有个最新热门话题“关于报BUG&#xff08;漏洞&#xff09;的礼仪”不要跟程序员说程序有BUG他们第一反应是&#xff1a;你的环境有问题吧&#xff1f;接着就是&#xff1a;XXX你会用吗&#xff01;&#xff08;此处不可描…

html li 做瀑布流,js实现瀑布流效果(自动生成新的内容)

当滚动条接近底部会自动生成新的内容(色块)效果图&#xff1a;代码如下&#xff1a;Title*{list-style: none;}div{overflow: hidden;}ul{float: left;}li{width:300px; margin-bottom:10px;}function rnd(n,m){return parseInt(Math.random()*(m-n))n;}function cl(){var li …

六元均匀直线阵的各元间距为_实验二 均匀直线阵

实验二均匀直线阵一、实验目的&#xff1a;通过MATLAB编程&#xff0c;了解均匀直线阵的辐射特性&#xff0c;熟悉影响天线阵辐射的各种因素及其产生的影响。二、实验环境&#xff1a;MATLAB软件三、实验原理&#xff1a;单个天线的方向性是有限的&#xff0c;为了加强天线的定…

jquery实现多行滚动效果

2019独角兽企业重金招聘Python工程师标准>>> 有时jquery博客想&#xff0c;整那么多demo有什么用呢&#xff1f; 有些前端新手朋友不会&#xff0c;为他们服务吧。还有喜欢自己留点字迹&#xff0c;也好方便自己回过头看看。 温故而知新嘛。 前端需要那么多js特效&a…

.NET 搭建简单的通知服务

搭建简单的通知服务Intro很多情况下&#xff0c;我们都会遇到一些需要进行通知报警的场景&#xff0c;比如说服务器资源监控报警&#xff0c;抢到火车票后通知用户进行付款。原来主要是用的钉钉群里的机器人来做的通知&#xff0c;周末看到原来做 【Server 酱】的大佬写了一个简…

systrace html空白,Android性能优化之Systrace工具介绍(一) _ Systrace生成的trace.html打开空白或者打不开的解决办法...

1.必须用Chrome打开2.在mac电脑上&#xff0c;可能Chrome打开也是空白&#xff0c;解决办法是&#xff1a;在chrome地址栏中输入”chrome:tracing”&#xff0c;然后点击load按钮load你的trace.html文件。Systrace简单介绍Systrace是Android4.1中新增的性能数据采样和分析工具。…

c#程序设计教程 唐大仕pdf_C# 添加PDF水印

概述一般我们在向文档添加水印时&#xff0c;会分为直接添加文字水印和加载图片添加图片水印两种情况。常见的&#xff0c;在添加文字水印时会多以声明文档版权、权威性的文字、标语或者名称等&#xff1b;同样的&#xff0c;图片水印也通常可以是某组织的LOGO、印章、或者其他…

电脑病毒竟然被程序员当宠物养!网友:这些都是我逝去的青春

全世界只有3.14 % 的人关注了数据与算法之美起电脑病毒&#xff0c;大家第一时间应该是想到的熊猫烧香&#xff0c;木马等等吧。很多电脑病毒破坏力惊人&#xff0c;熊猫烧香在当年也是让全国人民都陷入一种恐慌状态。但对于我们程序员来说&#xff0c;看过的病毒跟吃的米一样多…

安装 pptpd 服务

原文链接http://wiki.ubuntu.org.cn/index.php?titleVps&variantzh-cn#.E5.AE.89.E8.A3.85_PPTP_VPN 安装 pptpd 服务 执行如下命令安装&#xff0c;遇到提示输入 Y/n 时&#xff0c;输入 Y 回车即可&#xff1a; apt-get install pptpd配置 pptpd 为了避免和本地网络冲突…

.NET5 WPF进阶教程

↑↑↑ 点击左上角蓝字关注我&#xff0c;为您提供技术新动态。本期内容一、概要本系列将继《.net wpf快速入门教程》带领大家了解wpf&#xff0c;帮助各位在初级向中级过渡的中掌握基本该具备的能力。本系列视频长度大约在15分钟到30分钟左右&#xff0c;视频内容不仅仅会讲解…

python二维列表写入excel_用Python实现合并excel列表

python操作excel主要用到xlrd和xlwt这两个库&#xff0c;即xlrd是读excel&#xff0c;xlwt是写excel的库。安装xlrd模块&#xff0c;在安装好python的环境下&#xff0c;打开cmd&#xff0c;输入pip install xlrd 回车。安装好后&#xff0c;再输入pip install xlrd回车&#x…

C语言入门经典材料领走不谢!

小天从大学开始&#xff0c;便开启资料收集功能。近几年以大数据的飞速发展&#xff0c;计算机科技进入新的发展阶段&#xff0c;再加上日常的深入研究&#xff0c;小天收集整理了丰富的C语言资料&#xff0c;内容涵盖“入门经典”&#xff0c;“考试必备材料”等。截止到今天&…