ASP.NET Core 的檔案上傳File uploads in ASP.NET Core

作者:Steve SmithBy Steve Smith

ASP.NET MVC 動作支援上傳一或多個檔案,方法是針對較小的檔案使用簡單模型繫結,或針對較大的檔案使用資料流。ASP.NET MVC actions support uploading of one or more files using simple model binding for smaller files or streaming for larger files.

從 GitHub 檢視或下載範例View or download sample from GitHub

使用模型繫結上傳小型檔案Uploading small files with model binding

若要上傳小型檔案,您可以使用多部分 HTML 表單,或使用 JavaScript 建構 POST 要求。To upload small files, you can use a multi-part HTML form or construct a POST request using JavaScript. 使用支援多個已上傳檔案之 Razor 的範例表單如下所示:An example form using Razor, which supports multiple uploaded files, is shown below:

<form method="post" enctype="multipart/form-data" asp-controller="UploadFiles" asp-action="Index">
    <div class="form-group">
        <div class="col-md-10">
            <p>Upload one or more files using this form:</p>
            <input type="file" name="files" multiple>
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-10">
            <input type="submit" value="Upload">
        </div>
    </div>
</form>

若要支援檔案上傳,HTML 表單必須指定 multipart/form-dataenctypeIn order to support file uploads, HTML forms must specify an enctype of multipart/form-data. 上述 files 輸入項目支援上傳多個檔案。The files input element shown above supports uploading multiple files. 省略此輸入項目上的 multiple 屬性,只允許上傳單一檔案。Omit the multiple attribute on this input element to allow just a single file to be uploaded. 上述標記在瀏覽器中會轉譯為:The above markup renders in a browser as:

檔案上傳表單

使用 IFormFile 介面,可以透過模型繫結存取已上傳至伺服器的個別檔案。The individual files uploaded to the server can be accessed through Model Binding using the IFormFile interface. IFormFile 具有下列結構:IFormFile has the following structure:

public interface IFormFile
{
    string ContentType { get; }
    string ContentDisposition { get; }
    IHeaderDictionary Headers { get; }
    long Length { get; }
    string Name { get; }
    string FileName { get; }
    Stream OpenReadStream();
    void CopyTo(Stream target);
    Task CopyToAsync(Stream target, CancellationToken cancellationToken = null);
}

警告

不依賴或不信任無驗證的 FileName 屬性。Don't rely on or trust the FileName property without validation. FileName 屬性只應該用於顯示。The FileName property should only be used for display purposes.

使用模型繫結和 IFormFile 介面上傳檔案時,動作方法可以接受代表數個檔案的單一 IFormFileIEnumerable<IFormFile> (或 List<IFormFile>)。When uploading files using model binding and the IFormFile interface, the action method can accept either a single IFormFile or an IEnumerable<IFormFile> (or List<IFormFile>) representing several files. 下列範例會循環使用一或多個已上傳檔案、將它們儲存至本機檔案系統,並傳回已上傳檔案的總數和大小。The following example loops through one or more uploaded files, saves them to the local file system, and returns the total number and size of files uploaded.

警告:下列程式碼使用 GetTempFileName,如果建立 65535 個以上的檔案,但未刪除先前的暫存檔案,其會擲回 IOExceptionWarning: The following code uses GetTempFileName, which throws an IOException if more than 65535 files are created without deleting previous temporary files. 實際的應用程式應該刪除暫存檔案,或使用 GetTempPathGetRandomFileName 建立暫存檔案名稱。A real app should either delete temporary files or use GetTempPath and GetRandomFileName to create temporary file names. 65535 為以伺服器為依據的檔案限制數,因此伺服器上的另一個應用程式可以用掉所有 65535 個檔案。The 65535 files limit is per server, so another app on the server can use up all 65535 files.

[HttpPost("UploadFiles")]
public async Task<IActionResult> Post(List<IFormFile> files)
{
    long size = files.Sum(f => f.Length);

    // full path to file in temp location
    var filePath = Path.GetTempFileName();

    foreach (var formFile in files)
    {
        if (formFile.Length > 0)
        {
            using (var stream = new FileStream(filePath, FileMode.Create))
            {
                await formFile.CopyToAsync(stream);
            }
        }
    }

    // process uploaded files
    // Don't rely on or trust the FileName property without validation.

    return Ok(new { count = files.Count, size, filePath});
}

會先將使用 IFormFile 技術所上傳的檔案緩衝至記憶體或是網頁伺服器的磁碟上,再進行處理。Files uploaded using the IFormFile technique are buffered in memory or on disk on the web server before being processed. 在動作方法內,IFormFile 內容可以當成資料流形式來存取。Inside the action method, the IFormFile contents are accessible as a stream. 除了本機檔案系統之外,檔案還可以串流至 Azure Blob 儲存體Entity FrameworkIn addition to the local file system, files can be streamed to Azure Blob storage or Entity Framework.

若要使用 Entity Framework 將二進位檔案資料儲存至資料庫,請定義實體上類型 byte[] 的屬性:To store binary file data in a database using Entity Framework, define a property of type byte[] on the entity:

public class ApplicationUser : IdentityUser
{
    public byte[] AvatarImage { get; set; }
}

指定 IFormFile 類型的 viewmodel 屬性:Specify a viewmodel property of type IFormFile:

public class RegisterViewModel
{
    // other properties omitted

    public IFormFile AvatarImage { get; set; }
}

注意

IFormFile 可以直接用作動作方法參數或 viewmodel 屬性,如上所示。IFormFile can be used directly as an action method parameter or as a viewmodel property, as shown above.

IFormFile 複製至資料流,並將其儲存至位元組陣列:Copy the IFormFile to a stream and save it to the byte array:

// POST: /Account/Register
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model)
{
    ViewData["ReturnUrl"] = returnUrl;
    if  (ModelState.IsValid)
    {
        var user = new ApplicationUser 
        {
            UserName = model.Email,
            Email = model.Email
        };
        using (var memoryStream = new MemoryStream())
        {
            await model.AvatarImage.CopyToAsync(memoryStream);
            user.AvatarImage = memoryStream.ToArray();
        }
    // additional logic omitted

    // Don't rely on or trust the model.AvatarImage.FileName property 
    // without validation.
}

注意

將二進位資料儲存至關聯式資料庫時請小心,因為它可能會對效能造成不良影響。Use caution when storing binary data in relational databases, as it can adversely impact performance.

使用資料流上傳大型檔案Uploading large files with streaming

如果檔案上傳的大小或頻率造成應用程式的資源問題,請考慮串流檔案上傳,而不是緩衝整個檔案,而這與上述模型繫結方法相同。If the size or frequency of file uploads is causing resource problems for the app, consider streaming the file upload rather than buffering it in its entirety, as the model binding approach shown above does. 雖然使用 IFormFile 和模型繫結是比較簡單的解決方案,但是串流需要數個步驟才能正確地實作。While using IFormFile and model binding is a much simpler solution, streaming requires a number of steps to implement properly.

注意

如果有任何單一緩衝的檔案超過 64 KB,則會將其從 RAM 移至伺服器之磁碟上的暫存檔案。Any single buffered file exceeding 64KB will be moved from RAM to a temp file on disk on the server. 檔案上傳所使用的資源 (磁碟、RAM) 取決於並行檔案上傳次數和大小。The resources (disk, RAM) used by file uploads depend on the number and size of concurrent file uploads. 串流與效能的關聯不大,而是與規模的關聯較大。Streaming isn't so much about perf, it's about scale. 如果您嘗試要緩衝太多上傳,則記憶體或磁碟空間不足時,會損毀您的網站。If you try to buffer too many uploads, your site will crash when it runs out of memory or disk space.

下列範例示範如何使用 JavaScript/Angular 來串流至控制器動作。The following example demonstrates using JavaScript/Angular to stream to a controller action. 使用自訂篩選屬性並傳入 HTTP 標頭來產生檔案的 antiforgery 權杖,而不是傳入要求本文。The file's antiforgery token is generated using a custom filter attribute and passed in HTTP headers instead of in the request body. 因為動作方法會直接處理已上傳的資料,所以另一個篩選會停用模型繫結。Because the action method processes the uploaded data directly, model binding is disabled by another filter. 在動作內,會使用 MultipartReader 來讀取表單內容,以讀取每個個別 MultipartSection、處理檔案,或視需要儲存內容。Within the action, the form's contents are read using a MultipartReader, which reads each individual MultipartSection, processing the file or storing the contents as appropriate. 讀取所有區段之後,動作會執行它自己的模型繫結。Once all sections have been read, the action performs its own model binding.

初始動作會載入表單,並將 antiforgery 權杖儲存至 Cookie (透過 GenerateAntiforgeryTokenCookieForAjax 屬性):The initial action loads the form and saves an antiforgery token in a cookie (via the GenerateAntiforgeryTokenCookieForAjax attribute):

[HttpGet]
[GenerateAntiforgeryTokenCookieForAjax]
public IActionResult Index()
{
    return View();
}

此屬性使用 ASP.NET Core 的內建 Antiforgery 支援,來設定含要求權杖的 Cookie:The attribute uses ASP.NET Core's built-in Antiforgery support to set a cookie with a request token:

public class GenerateAntiforgeryTokenCookieForAjaxAttribute : ActionFilterAttribute
{
    public override void OnActionExecuted(ActionExecutedContext context)
    {
        var antiforgery = context.HttpContext.RequestServices.GetService<IAntiforgery>();

        // We can send the request token as a JavaScript-readable cookie, 
        // and Angular will use it by default.
        var tokens = antiforgery.GetAndStoreTokens(context.HttpContext);
        context.HttpContext.Response.Cookies.Append(
            "XSRF-TOKEN",
            tokens.RequestToken,
            new CookieOptions() { HttpOnly = false });
    }
}

角度會在名為 X-XSRF-TOKEN 的要求標頭中自動傳遞 antiforgery 權杖。Angular automatically passes an antiforgery token in a request header named X-XSRF-TOKEN. ASP.NET Core MVC 應用程式設定成在 Startup.cs 的其組態中參照此標頭:The ASP.NET Core MVC app is configured to refer to this header in its configuration in Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    // Angular's default header name for sending the XSRF token.
    services.AddAntiforgery(options => options.HeaderName = "X-XSRF-TOKEN");

    services.AddMvc();
}

下述中的 DisableFormValueModelBinding 屬性是用來停用 Upload 動作方法的模型繫結。 The DisableFormValueModelBinding attribute, shown below, is used to disable model binding for the Upload action method.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
    public void OnResourceExecuting(ResourceExecutingContext context)
    {
        var factories = context.ValueProviderFactories;
        factories.RemoveType<FormValueProviderFactory>();
        factories.RemoveType<JQueryFormValueProviderFactory>();
    }

    public void OnResourceExecuted(ResourceExecutedContext context)
    {
    }
}    

因為已停用模型繫結,所以 Upload 動作方法不會接受參數。Since model binding is disabled, the Upload action method doesn't accept parameters. 它會直接使用 ControllerBaseRequest 屬性。It works directly with the Request property of ControllerBase. MultipartReader 是用來讀取每個區段。A MultipartReader is used to read each section. 檔案會與 GUID 檔案名稱一起儲存,而索引鍵/值資料會儲存至 KeyValueAccumulatorThe file is saved with a GUID filename and the key/value data is stored in a KeyValueAccumulator. 讀取所有區段之後,會使用 KeyValueAccumulator 的內容,將表單資料繫結至模型類型。Once all sections have been read, the contents of the KeyValueAccumulator are used to bind the form data to a model type.

完整 Upload 方法如下:The complete Upload method is shown below:

警告:下列程式碼使用 GetTempFileName,如果建立 65535 個以上的檔案,但未刪除先前的暫存檔案,其會擲回 IOExceptionWarning: The following code uses GetTempFileName, which throws an IOException if more than 65535 files are created without deleting previous temporary files. 實際的應用程式應該刪除暫存檔案,或使用 GetTempPathGetRandomFileName 建立暫存檔案名稱。A real app should either delete temporary files or use GetTempPath and GetRandomFileName to create temporary file names. 65535 為以伺服器為依據的檔案限制數,因此伺服器上的另一個應用程式可以用掉所有 65535 個檔案。The 65535 files limit is per server, so another app on the server can use up all 65535 files.

// 1. Disable the form value model binding here to take control of handling 
//    potentially large files.
// 2. Typically antiforgery tokens are sent in request body, but since we 
//    do not want to read the request body early, the tokens are made to be 
//    sent via headers. The antiforgery token filter first looks for tokens
//    in the request header and then falls back to reading the body.
[HttpPost]
[DisableFormValueModelBinding]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Upload()
{
    if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
    {
        return BadRequest($"Expected a multipart request, but got {Request.ContentType}");
    }

    // Used to accumulate all the form url encoded key value pairs in the 
    // request.
    var formAccumulator = new KeyValueAccumulator();
    string targetFilePath = null;

    var boundary = MultipartRequestHelper.GetBoundary(
        MediaTypeHeaderValue.Parse(Request.ContentType),
        _defaultFormOptions.MultipartBoundaryLengthLimit);
    var reader = new MultipartReader(boundary, HttpContext.Request.Body);

    var section = await reader.ReadNextSectionAsync();
    while (section != null)
    {
        ContentDispositionHeaderValue contentDisposition;
        var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition);

        if (hasContentDispositionHeader)
        {
            if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
            {
                targetFilePath = Path.GetTempFileName();
                using (var targetStream = System.IO.File.Create(targetFilePath))
                {
                    await section.Body.CopyToAsync(targetStream);

                    _logger.LogInformation($"Copied the uploaded file '{targetFilePath}'");
                }
            }
            else if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition))
            {
                // Content-Disposition: form-data; name="key"
                //
                // value

                // Do not limit the key name length here because the 
                // multipart headers length limit is already in effect.
                var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name);
                var encoding = GetEncoding(section);
                using (var streamReader = new StreamReader(
                    section.Body,
                    encoding,
                    detectEncodingFromByteOrderMarks: true,
                    bufferSize: 1024,
                    leaveOpen: true))
                {
                    // The value length limit is enforced by MultipartBodyLengthLimit
                    var value = await streamReader.ReadToEndAsync();
                    if (String.Equals(value, "undefined", StringComparison.OrdinalIgnoreCase))
                    {
                        value = String.Empty;
                    }
                    formAccumulator.Append(key, value);

                    if (formAccumulator.ValueCount > _defaultFormOptions.ValueCountLimit)
                    {
                        throw new InvalidDataException($"Form key count limit {_defaultFormOptions.ValueCountLimit} exceeded.");
                    }
                }
            }
        }

        // Drains any remaining section body that has not been consumed and
        // reads the headers for the next section.
        section = await reader.ReadNextSectionAsync();
    }

    // Bind form data to a model
    var user = new User();
    var formValueProvider = new FormValueProvider(
        BindingSource.Form,
        new FormCollection(formAccumulator.GetResults()),
        CultureInfo.CurrentCulture);

    var bindingSuccessful = await TryUpdateModelAsync(user, prefix: "",
        valueProvider: formValueProvider);
    if (!bindingSuccessful)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }
    }

    var uploadedData = new UploadedData()
    {
        Name = user.Name,
        Age = user.Age,
        Zipcode = user.Zipcode,
        FilePath = targetFilePath
    };
    return Json(uploadedData);
}

疑難排解Troubleshooting

以下是使用上傳檔案和其可能解決方案時發現的一些常見問題。Below are some common problems encountered when working with uploading files and their possible solutions.

IIS 的未預期找不到錯誤Unexpected Not Found error with IIS

下列錯誤指出您的檔案上傳超過伺服器所設定的 maxAllowedContentLengthThe following error indicates your file upload exceeds the server's configured maxAllowedContentLength:

HTTP 404.13 - Not Found
The request filtering module is configured to deny a request that exceeds the request content length.

預設設定是 30000000,其大約是 28.6 MB。The default setting is 30000000, which is approximately 28.6MB. 編輯 web.config,即可自訂值:The value can be customized by editing web.config:

<system.webServer>
  <security>
    <requestFiltering>
      <!-- This will handle requests up to 50MB -->
      <requestLimits maxAllowedContentLength="52428800" />
    </requestFiltering>
  </security>
</system.webServer>

這個設定只適用於 IIS。This setting only applies to IIS. 在 Kestrel 上裝載時,預設不會發生此行為。The behavior doesn't occur by default when hosting on Kestrel. 如需詳細資訊,請參閱要求限制<requestLimits>For more information, see Request Limits <requestLimits>.

IFormFile 的 Null 參考例外狀況Null Reference Exception with IFormFile

如果您的控制器接受使用 IFormFile 的已上傳檔案,但您發現值一律是 Null,則請確認 HTML 表單將會指定 multipart/form-dataenctype 值。If your controller is accepting uploaded files using IFormFile but you find that the value is always null, confirm that your HTML form is specifying an enctype value of multipart/form-data. 如果未在 <form> 項目上設定此屬性,則不會進行檔案上傳,而且任何繫結的 IFormFile 引數都會是 Null。If this attribute isn't set on the <form> element, the file upload won't occur and any bound IFormFile arguments will be null.