分享web开发知识

注册/登录|最近发布|今日推荐

主页 IT知识网页技术软件开发前端开发代码编程运营维护技术分享教程案例
当前位置:首页 > 网页技术

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

发布时间:2023-09-06 02:18责任编辑:郭大石关键词:.NETMVC文件上传

用文件模型绑定接口:IFormFile (小文件上传)


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

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

<form method="post" enctype="multipart/form-data" action="/Upload"> ???<div> ???????<p>Upload one or more files using this form:</p> ???????<input type="file" name="files" /> ???</div> ???<div> ????????<input type="submit" value="Upload" /> ???</div></form>

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

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

[HttpPost]public async Task<IActionResult> Post(List<IFormFile> 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-----------------------------99614912995Content-Disposition: form-data; name="SOMENAME"Formulaire de Quota-----------------------------99614912995Content-Disposition: form-data; name="OTHERNAME"SOMEDATA-----------------------------99614912995Content-Disposition: form-data; name="files"; filename="Misc 001.jpg"SDFESDSDSDJXCK+DSDSDSSDSFDFDF423232DASDSDSDFDSFJHSIHFSDUIASUI+/==-----------------------------99614912995Content-Disposition: form-data; name="files"; filename="Misc 002.jpg"ASAADSDSDJXCKDSDSDSHAUSAUASAASSDSDFDSFJHSIHFSDUIASUI+/==-----------------------------99614912995Content-Disposition: form-data; name="files"; filename="Misc 003.jpg"TGUHGSDSDJXCK+DSDSDSSDSFDFDSAOJDIOASSAADDASDASDASSADASDSDSDSDFDSFJHSIHFSDUIASUI+/==-----------------------------99614912995--

这就是一个multipart/form-data格式的Http请求,我们可以看到里面第一行信息是Content-Type,这和我们在html页面中form标签上的enctype属性值一致,第一行中接着有一个boundary=---------------------------99614912995,boundary=后面的值是随机生成的,这个其实是在声明Http请求中表单数据的分隔符是什么,其代表的是在Http请求中每读到一行 ---------------------------99614912995,表示一份section数据,一份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."); ???????????} ???????????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; ???????} ???????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" ???????} ???????????????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<FormValueProvider> 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 ???????????????????POSTDATA =-----------------------------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 ???????????????????POSTDATA =-----------------------------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<FormValueProviderFactory>() ???????????.FirstOrDefault(); ???????if (formValueProviderFactory != null) ???????{ ???????????context.ValueProviderFactories.Remove(formValueProviderFactory); ???????} ????????var jqueryFormValueProviderFactory = context.ValueProviderFactories ???????????.OfType<JQueryFormValueProviderFactory>() ???????????.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<IActionResult> 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页面和前面几乎一样:

<form method="post" enctype="multipart/form-data" action="/Home/Index"> ???<div> ???????<p>Upload one or more files using this form:</p> ???????<input type="file" name="files" multiple /> ???</div> ???<div> ???????<p>Your Username</p> ???????<input type="text" name="username" /> ???</div> ???<div> ????????<input type="submit" value="Upload" /> ???</div></form>

这就是所有的代码,希望对大家有所帮助!

参考文献:

File uploads in ASP.NET Core

Uploading Files In ASP.net Core

What is the boundary parameter in an HTTP multi-part (POST) Request?

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

原文地址:https://www.cnblogs.com/OpenCoder/p/9785031.html

知识推荐

我的编程学习网——分享web前端后端开发技术知识。 垃圾信息处理邮箱 tousu563@163.com 网站地图
icp备案号 闽ICP备2023006418号-8 不良信息举报平台 互联网安全管理备案 Copyright 2023 www.wodecom.cn All Rights Reserved