.NET Core 如何上传文件及处理大文件上传

.NET Core 如何上传文件及处理大文件上传

当你使用IFormFile接口来上传文件的时候,一定要注意,IFormFile会将一个Http请求中的所有文件都读取到服务器内存后,才会触发ASP.NET Core MVC的Controller中的Action方法。这种情况下,如果上传一些小文件是没问题的,但是如果上传大文件,势必会造成服务器内存大量被占用甚至溢出,所以IFormFile接口只适合小文件上传。

一个文件上传页面的Html代码一般如下所示:

Upload one or more files using this form:

为了支持文件上传,form标签上一定要记得声明属性enctype="multipart/form-data",否者你会发现ASP.NET Core MVC的Controller中死活都读不到任何文件。Input type="file"标签在html 5中支持上传多个文件,加上属性multiple即可。

使用IFormFile接口上传文件非常简单,将其声明为Contoller中Action的集合参数即可:

[HttpPost]

public async Task Post(List files)

{

long size = files.Sum(f => f.Length);

foreach (var formFile in files)

{

var filePath = @"D:\UploadingFiles\" + formFile.FileName;

if (formFile.Length > 0)

{

using (var stream = new FileStream(filePath, FileMode.Create))

{

await formFile.CopyToAsync(stream);

}

}

}

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

}

注意上面Action方法Post的参数名files,必须要和上传页面中的Input type="file"标签的name属性值一样。

用文件流 (大文件上传)

在介绍这个方法之前我们先来看看一个包含上传文件的Http请求是什么样子的:

Content-Type=multipart/form-data; boundary=---------------------------99614912995

-----------------------------99614912995

Content-Disposition: form-data; name="SOMENAME"

Formulaire de Quota

-----------------------------99614912995

Content-Disposition: form-data; name="OTHERNAME"

SOMEDATA

-----------------------------99614912995

Content-Disposition: form-data; name="files"; filename="Misc 001.jpg"

SDFESDSDSDJXCK+DSDSDSSDSFDFDF423232DASDSDSDFDSFJHSIHFSDUIASUI+/==

-----------------------------99614912995

Content-Disposition: form-data; name="files"; filename="Misc 002.jpg"

ASAADSDSDJXCKDSDSDSHAUSAUASAASSDSDFDSFJHSIHFSDUIASUI+/==

-----------------------------99614912995

Content-Disposition: form-data; name="files"; filename="Misc 003.jpg"

TGUHGSDSDJXCK+DSDSDSSDSFDFDSAOJDIOASSAADDASDASDASSADASDSDSDSDFDSFJHSIHFSDUIASUI+/==

-----------------------------99614912995--

这就是一个multipart/form-data格式的Http请求,我们可以看到第一行信息是Http header,这里我们只列出了Content-Type这一行Http header信息,这和我们在html页面中form标签上的enctype属性值一致,第一行中接着有一个boundary=---------------------------99614912995,boundary=后面的值是随机生成的,这个其实是在声明Http请求中表单数据的分隔符是什么,其代表的是在Http请求中每读到一行 ---------------------------99614912995,表示一个section数据,一个section有可能是一个表单的键值数据,也有可能是一个上传文件的文件数据。每个section的第一行是section header,其中Content-Disposition属性都为form-data,表示这个section来自form标签提交的表单数据,如果section header拥有filename或filenamestar属性,那么表示这个section是一个上传文件的文件数据,否者这个section是一个表单的键值数据,section header之后的行就是这个section真正的数据行。例如我们上面的例子中,前两个section就是表单键值对,后面三个section是三个上传的图片文件。

那么接下来,我们来看看怎么用文件流来上传大文件,避免一次性将所有上传的文件都加载到服务器内存中。用文件流来上传比较麻烦的地方在于你无法使用ASP.NET Core MVC的模型绑定器来将上传文件反序列化为C#对象(如同前面介绍的IFormFile接口那样)。首先我们需要定义类MultipartRequestHelper,用于识别Http请求中的各个section类型(是表单键值对section,还是上传文件section)

using System;

using System.IO;

using Microsoft.Net.Http.Headers;

namespace AspNetCore.MultipartRequest

{

public static class MultipartRequestHelper

{

// Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"

// The spec says 70 characters is a reasonable limit.

public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)

{

//var boundary = Microsoft.Net.Http.Headers.HeaderUtilities.RemoveQuotes(contentType.Boundary);// .NET Core <2.0

var boundary = Microsoft.Net.Http.Headers.HeaderUtilities.RemoveQuotes(contentType.Boundary).Value; //.NET Core 2.0

if (string.IsNullOrWhiteSpace(boundary))

{

throw new InvalidDataException("Missing content-type boundary.");

}

//注意这里的boundary.Length指的是boundary=---------------------------99614912995中等号后面---------------------------99614912995字符串的长度,也就是section分隔符的长度,上面也说了这个长度一般不会超过70个字符是比较合理的

if (boundary.Length > lengthLimit)

{

throw new InvalidDataException(

$"Multipart boundary length limit {lengthLimit} exceeded.");

}

return boundary;

}

public static bool IsMultipartContentType(string contentType)

{

return !string.IsNullOrEmpty(contentType)

&& contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;

}

//如果section是表单键值对section,那么本方法返回true

public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)

{

// Content-Disposition: form-data; name="key";

return contentDisposition != null

&& contentDisposition.DispositionType.Equals("form-data")

&& string.IsNullOrEmpty(contentDisposition.FileName.Value) // For .NET Core <2.0 remove ".Value"

&& string.IsNullOrEmpty(contentDisposition.FileNameStar.Value); // For .NET Core <2.0 remove ".Value"

}

//如果section是上传文件section,那么本方法返回true

public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)

{

// Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"

return contentDisposition != null

&& contentDisposition.DispositionType.Equals("form-data")

&& (!string.IsNullOrEmpty(contentDisposition.FileName.Value) // For .NET Core <2.0 remove ".Value"

|| !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value)); // For .NET Core <2.0 remove ".Value"

}

// 如果一个section的Header是: Content-Disposition: form-data; name="files"; filename="Misc 002.jpg"

// 那么本方法返回: files

public static string GetFileContentInputName(ContentDispositionHeaderValue contentDisposition)

{

return contentDisposition.Name.Value;

}

// 如果一个section的Header是: Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"

// 那么本方法返回: Misc 002.jpg

public static string GetFileName(ContentDispositionHeaderValue contentDisposition)

{

return contentDisposition.FileName.Value;

}

}

}

然后我们需要定义一个扩展类叫FileStreamingHelper,其中的StreamFiles扩展方法用于读取上传文件的文件流数据,并且将数据写入到服务器的硬盘上,其接受一个参数targetDirectory,用于声明将上传文件存储到服务器的哪个文件夹下。

using Microsoft.AspNetCore.Http;

using Microsoft.AspNetCore.Http.Features;

using Microsoft.AspNetCore.Mvc.ModelBinding;

using Microsoft.AspNetCore.WebUtilities;

using Microsoft.Net.Http.Headers;

using System;

using System.Globalization;

using System.IO;

using System.Text;

using System.Threading.Tasks;

namespace AspNetCore.MultipartRequest

{

public static class FileStreamingHelper

{

private static readonly FormOptions _defaultFormOptions = new FormOptions();

public static async Task StreamFiles(this HttpRequest request, string targetDirectory)

{

if (!MultipartRequestHelper.IsMultipartContentType(request.ContentType))

{

throw new Exception($"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();

var boundary = MultipartRequestHelper.GetBoundary(

MediaTypeHeaderValue.Parse(request.ContentType),

_defaultFormOptions.MultipartBoundaryLengthLimit);

var reader = new MultipartReader(boundary, request.Body);

var section = await reader.ReadNextSectionAsync();//用于读取Http请求中的第一个section数据

while (section != null)

{

ContentDispositionHeaderValue contentDisposition;

var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition);

if (hasContentDispositionHeader)

{

/*

用于处理上传文件类型的的section

-----------------------------99614912995

Content - Disposition: form - data; name = "files"; filename = "Misc 002.jpg"

ASAADSDSDJXCKDSDSDSHAUSAUASAASSDSDFDSFJHSIHFSDUIASUI+/==

-----------------------------99614912995

*/

if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))

{

if (!Directory.Exists(targetDirectory))

{

Directory.CreateDirectory(targetDirectory);

}

var fileName = MultipartRequestHelper.GetFileName(contentDisposition);

var loadBufferBytes = 1024;//这个是每一次从Http请求的section中读出文件数据的大小,单位是Byte即字节,这里设置为1024的意思是,每次从Http请求的section数据流中读取出1024字节的数据到服务器内存中,然后写入下面targetFileStream的文件流中,可以根据服务器的内存大小调整这个值。这样就避免了一次加载所有上传文件的数据到服务器内存中,导致服务器崩溃。

using (var targetFileStream = System.IO.File.Create(targetDirectory + "\\" + fileName))

{

//section.Body是System.IO.Stream类型,表示的是Http请求中一个section的数据流,从该数据流中可以读出每一个section的全部数据,所以我们下面也可以不用section.Body.CopyToAsync方法,而是在一个循环中用section.Body.Read方法自己读出数据,再将数据写入到targetFileStream

await section.Body.CopyToAsync(targetFileStream, loadBufferBytes);

}

}

/*

用于处理表单键值数据的section

-----------------------------99614912995

Content - Disposition: form - data; name = "SOMENAME"

Formulaire de Quota

-----------------------------99614912995

*/

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, value); // For .NET Core <2.0 remove ".Value" from key

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();//用于读取Http请求中的下一个section数据

}

// Bind form data to a model

var formValueProvider = new FormValueProvider(

BindingSource.Form,

new FormCollection(formAccumulator.GetResults()),

CultureInfo.CurrentCulture);

return formValueProvider;

}

private static Encoding GetEncoding(MultipartSection section)

{

MediaTypeHeaderValue mediaType;

var hasMediaTypeHeader = MediaTypeHeaderValue.TryParse(section.ContentType, out mediaType);

// UTF-7 is insecure and should not be honored. UTF-8 will succeed in

// most cases.

if (!hasMediaTypeHeader || Encoding.UTF7.Equals(mediaType.Encoding))

{

return Encoding.UTF8;

}

return mediaType.Encoding;

}

}

}

现在我们还需要创建一个ASP.NET Core MVC的自定义拦截器DisableFormValueModelBindingAttribute,该拦截器实现接口IResourceFilter,用来禁用ASP.NET Core MVC的模型绑定器,这样当一个Http请求到达服务器后,ASP.NET Core MVC就不会在将请求的所有上传文件数据都加载到服务器内存后,才执行Contoller的Action方法,而是当Http请求到达服务器时,就立刻执行Contoller的Action方法。

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]

public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter

{

public void OnResourceExecuting(ResourceExecutingContext context)

{

var formValueProviderFactory = context.ValueProviderFactories

.OfType()

.FirstOrDefault();

if (formValueProviderFactory != null)

{

context.ValueProviderFactories.Remove(formValueProviderFactory);

}

var jqueryFormValueProviderFactory = context.ValueProviderFactories

.OfType()

.FirstOrDefault();

if (jqueryFormValueProviderFactory != null)

{

context.ValueProviderFactories.Remove(jqueryFormValueProviderFactory);

}

}

public void OnResourceExecuted(ResourceExecutedContext context)

{

}

}

最后我们在Contoller中定义一个叫Index的Action方法,并注册我们定义的DisableFormValueModelBindingAttribute拦截器,来禁用Action的模型绑定。Index方法会调用我们前面定义的FileStreamingHelper类中的StreamFiles方法,其参数为用来存储上传文件的文件夹路径。StreamFiles方法会返回一个FormValueProvider,用来存储Http请求中的表单键值数据,之后我们会将其绑定到MVC的视图模型viewModel上,然后将viewModel传回给客户端浏览器,来告述客户端浏览器文件上传成功。

[HttpPost]

[DisableFormValueModelBinding]

public async Task Index()

{

FormValueProvider formModel;

formModel = await Request.StreamFiles(@"D:\UploadingFiles");

var viewModel = new MyViewModel();

var bindingSuccessful = await TryUpdateModelAsync(viewModel, prefix: "",

valueProvider: formModel);

if (!bindingSuccessful)

{

if (!ModelState.IsValid)

{

return BadRequest(ModelState);

}

}

return Ok(viewModel);

}

视图模型viewModel的定义如下:

public class MyViewModel

{

public string Username { get; set; }

}

最后我们用于上传文件的html页面和前面几乎一样:

Upload one or more files using this form:

Your Username

到这里 上传大文件时提示404

在创建的项目里面是没有 “web.config” 文件的。

上传大文件时需要配置下文件的大小,需要在 “config” 文件里配置。创建一个或复制一个 “web.config”,代码:

然后在 Startup.cs 文件中代码如下:

public void ConfigureServices(IServiceCollection services)

{

//设置接收文件长度的最大值。

services.Configure(x =>

{

x.ValueLengthLimit = int.MaxValue;

x.MultipartBodyLengthLimit = int.MaxValue;

x.MultipartHeadersLengthLimit = int.MaxValue;

});

services.AddMvc();

}

相关推荐

内存卡检测工具 2.50 最新版
365bet网站

内存卡检测工具 2.50 最新版

📅 07-02 👁️ 5083
方舟:生存进化 建筑的摧毁时间计算 计算方法
365买球官网入口

方舟:生存进化 建筑的摧毁时间计算 计算方法

📅 07-02 👁️ 1436
为什么有的女孩手很小
beat365中国在线体育

为什么有的女孩手很小

📅 06-30 👁️ 2636
不小心吃了蚊子怎么办
365bet网站

不小心吃了蚊子怎么办

📅 06-27 👁️ 2947
2025 年 12 个最好用的种子站 (安全可用)
365买球官网入口

2025 年 12 个最好用的种子站 (安全可用)

📅 07-04 👁️ 3589
悲剧!意大利60年后再度无缘世界杯,布冯流泪退出国家队
beat365中国在线体育

悲剧!意大利60年后再度无缘世界杯,布冯流泪退出国家队

📅 07-03 👁️ 5163
日本米饭为何特别美味?日本人爱用煮饭锅具彻底比较
beat365中国在线体育

日本米饭为何特别美味?日本人爱用煮饭锅具彻底比较

📅 07-03 👁️ 5834
为什么唱歌不好听?了解这三个重点解决你的唱歌问题
粽子詳細做法,從準備餡料到燒粽子,一步一步教你做!今年端午節,自己動手燒粽子吧