ASP.NET Core檔案上傳IFormFile於Request.Body的羈絆

yi念之間發表於2021-05-31

前言

    在上篇文章深入探究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的關鍵所在。

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

?歡迎掃碼關注我的公眾號? ASP.NET Core檔案上傳IFormFile於Request.Body的羈絆

相關文章