ASP.NET Core文件上傳IFormFile於Request.Body的羈絆

前言

    在上篇文章深入探究ASP.NET Core讀取Request.Body的正確方式中我們探討了很多人在日常開發中經常遇到的也是最基礎的問題,那就是關於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的方式直接得到上傳文件的Stream
        StreamReader 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 =>
    {
        // 設置上傳大小限制256MB
        options.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大小限制256MB
                        options.Limits.MaxRequestBodySize = 268435456;
                    });
                    webBuilder.UseStartup<Startup>();
                });

很多時候這兩處設置都需要配合着一起使用,才能達到效果,用的時候需要特別的留意一下。

源碼探究

上面我們大致演示了IFormFile的基礎操作,我們上面的演示大致劃分爲兩類,一種是通過模型綁定的方式而這種方式包含了IFormFileList<IFormFile>IFormFileCollection三種方式 ,另一種是通過Request.Form.Files的方式,爲了搞懂他們的關係,就必須從模型綁定下手。

始於模型綁定

首先我們找到關於操作FormFile相關操作模型綁定的地方在FormFileModelBinder類的BindModelAsync方法[點擊查看源碼👈]我們看到了如下代碼,展示的代碼刪除了部分邏輯,提取的是涉及到我們要關注的流程性的操作

public async Task BindModelAsync(ModelBindingContext bindingContext)
{
    //獲取要綁定的參數類型
    var createFileCollection = bindingContext.ModelType == typeof(IFormFileCollection);

    //判斷模型綁定參數類型是IFormFileCollection類型或可兼容IFormFileCollection類型
    //其中ModelBindingHelper.CanGetCompatibleCollection是用來判斷模型綁定參數是否可以兼容IFormFileCollection
    if (!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;
    //如果模型參數爲IFormFile
    if (bindingContext.ModelType == typeof(IFormFile))
    {
        //並未獲取上傳文件相關直接返回
        if (postedFiles.Count == 0)
        {
            return;
        }
        //集合存在則獲取第一個
        value = postedFiles.First();
    }
    else
    {
        //如果模型參數不爲IFormFile
        if (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初始化FileCollection
        else 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模型綁定幫助類[點擊查看源碼👈]相關的方法,主要是封裝模型綁定公共的幫助類。涉及到的我們需要的方法邏輯,上面備註已經說明了,這裏就不展示源碼了,因爲它對於我們的流程來說並不核心。

上面我們看到了用於初始化綁定集合的核心操作是GetFormFilesAsync方法[點擊查看源碼👈]話不多說我們來直接看下它的實現邏輯

private async Task GetFormFilesAsync(
            string modelName,
            ModelBindingContext bindingContext,
            ICollection<IFormFile> postedFiles)
{
    //獲取Request實例
    var request = bindingContext.HttpContext.Request;
    if (request.HasFormContentType)
    {
        //獲取Request.Form
        var form = await request.ReadFormAsync();
        //遍歷Request.Form.Files
        foreach (var file in form.Files)
        {
            //FileName如果未空的話不進行模型綁定
            if (file.Length == 0 && string.IsNullOrEmpty(file.FileName))
            {
                continue;
            }
            //FileName等於模型綁定名稱的話則添加postedFiles
            if (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擴展類[點擊查看源碼👈]大致代碼如下

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的時候初始化一個FormFeature
    if (formFeature == null || formFeature.Form == null)
    {
        features.Set<IFormFeature>(new FormFeature(request, options));
    }
    //調用了HttpRequest的ReadFormAsync方法
    return request.ReadFormAsync(cancellationToken);
}

沒啥可說的直接找到HttpRequest的ReadFormAsync方法,我們在上篇文章瞭解過HttpRequest抽象類默認的實現類是DefaultHttpRequest,所以我們找到DefaultHttpRequest的ReadFormAsync方法[點擊查看源碼👈]看一下它的實現

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方法[點擊查看源碼👈]找到源碼大致如下所示

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的邏輯,這裏我們也就只保留這類的相關邏輯省去了其他的邏輯,有需要了解的同學可以自行查看源碼[點擊查看源碼👈]

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);
            // 把針對文件上傳的部分封裝到MultipartReader
            var multipartReader = new MultipartReader(boundary, _request.Body)
            {
                //Header個數限制
                HeadersCountLimit = _options.MultipartHeadersCountLimit,
                //Header長度限制
                HeadersLengthLimit = _options.MultipartHeadersLengthLimit,
                //Body長度限制
                BodyLengthLimit = _options.MultipartBodyLengthLimit,
            };

            //獲取下一個可解析的節點,可以理解爲每一個要解析的上傳文件信息
            var section = await multipartReader.ReadNextSectionAsync(cancellationToken);
            //不爲null說明已從Body解析出的上傳文件信息
            while (section != null)
            {
                // 在這裏解析內容配置並進一步傳遞它以避免重新分析
                if (!ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out var contentDisposition))
                {
                    throw new InvalidDataException("");
                }

                if (contentDisposition.IsFileDisposition())
                {
                    var fileSection = new FileMultipartSection(section, contentDisposition);
                    // 如果尚未對整個正文執行緩衝,則爲文件啓用緩衝
                    section.EnableRewind(
                        _request.HttpContext.Response.RegisterForDispose,
                        _options.MemoryBufferThreshold, _options.MultipartBodyLengthLimit);

                    // 找到結尾
                    await section.Body.DrainAsync(cancellationToken);

                    var name = fileSection.Name;
                    var fileName = fileSection.FileName;

                    FormFile file;
                    //判斷Body默認的流是否被修改過,比如開啓緩衝就會修改
                    //如果Body不是默認流則直接服務Body
                    if (section.BaseStreamOffset.HasValue)
                    {
                        file = new FormFile(_request.Body, section.BaseStreamOffset.GetValueOrDefault(), section.Body.Length, name, fileName);
                    }
                    else
                    {
                        // 如果沒有被修改過則獲取MultipartReaderStream的實例
                        file = new FormFile(section.Body, 0, section.Body.Length, name, fileName);
                    }
                    file.Headers = new HeaderDictionary(section.Headers);

                    //如果解析出來了文件信息則初始化FormFileCollection
                    if (files == null)
                    {
                        files = new FormFileCollection();
                    }
                    if (files.Count >= _options.ValueCountLimit)
                    {
                        throw new InvalidDataException("");
                    }
                    files.Add(file);
                }
                else if (contentDisposition.IsFormDisposition())
                {
                    var formDataSection = new FormMultipartSection(section, contentDisposition);

                    var key = formDataSection.Name;
                    var value = await formDataSection.GetValueAsync();

                    formAccumulator.Append(key, value);
                    if (formAccumulator.ValueCount > _options.ValueCountLimit)
                    {
                        throw new InvalidDataException("");
                    }
                }
                else
                {
                    //沒解析出來類型
                }
                section = 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得到FormCollection
    if (files != null)
    {
        Form = new FormCollection(null, files);
    }
    return Form;
}

這部分源碼比較多,而且這還是精簡過只剩下ContentTypemultipart/form-data的內容,不過從這裏我們就可以看出來FormFile的實例確實是依靠Request的Body裏。其核心就在MultipartReader類的ReadNextSectionAsync方法返回的Section數據[點擊查看源碼👈]通過上面的循環可以看到它是循環讀取的,它通過解析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 };
    //判斷流是否是原始的HttpRequestStream
    long? baseStreamOffset = _stream.CanSeek ? (long?)_stream.Position : null;
    //通過上面信息構造MultipartSection實例
    return new MultipartSection() { Headers = headers, Body = _currentStream, BaseStreamOffset = baseStreamOffset };
}

這裏可以看出傳遞下來的RequestBody被構建出了MultipartReaderStream實例,即MultipartReaderStream包裝了RequestBody中的信息[點擊查看源碼👈]看名字也知道它也是實現了Stream抽象類

internal sealed class MultipartReaderStream : Stream
{
}

而且我們看到BodyLengthLimit正是傳遞給了它的LengthLimit屬性,而BodyLengthLimit正是設置限制上傳文件的大小的屬性,我們找到使用LengthLimit屬性的地方,代碼如下所示[點擊查看源碼👈]

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方法中調用[點擊查看源碼👈]這樣才能起到限制的作用,大致看一下Read方法的實現代碼

public override int Read(byte[] buffer, int offset, int count)
{
    //如果已經讀到了結尾則直接返回0
    if (_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));
            //返回讀取的長度正是調用的UpdatePosition
            return 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));
    //這裏同樣是UpdatePosition
    return UpdatePosition(read);
}

通過這裏就可清楚的看到MultipartReaderStream的Read方法就是在解析讀取的RequestBody的FormData類型的信息,解析成我們可以直接讀取或者直接保存成文件的的原始的文件信息,它還有一個異步讀取的ReadAsync方法其實現原理類似,在這裏咱們就不在展示源碼了。最後我們再來看一下MultipartSection類的實現[點擊查看源碼👈]我們上面知道了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的關鍵所在。

到了這裏本文的全部內容就差不多結束了,希望本文能給大家帶來收穫。我覺得有時候看源碼能解決許多問題和心中的疑惑,因爲我們作爲程序員每天寫的也就是代碼,所以沒有比程序員直接讀取代碼能更好的瞭解想了解的信息了。但是讀源碼也有一定的困難,畢竟是別人的代碼,思維存在一定的偏差,更何況是一些優秀的框架,作者們的思維很可能比我們要高出很多,所以很多時候讀起來會非常的喫力,即便如此筆者也覺得讀源碼是瞭解框架得到框架信息的一種比較行之有效的方式。

👇歡迎掃碼關注我的公衆號👇
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章