随着技术的发展,ASP.NET Core MVC也推出了好长时间,经过不断的版本更新迭代,已经越来越完善,本系列文章主要讲解ASP.NET Core MVC开发B/S系统过程中所涉及到的相关内容,适用于初学者,在校毕业生,或其他想从事ASP.NET Core MVC 系统开发的人员。 经过前几篇文章的讲解,初步了解ASP.NET Core MVC项目创建,启动运行,以及命名约定,创建控制器,视图,模型,接收参数,传递数据ViewData,ViewBag,路由,页面布局,wwwroot和客户端库,Razor语法,EnityFrameworkCore与数据库,HttpContext,Request,Response,Session,序列化等内容,今天继续讲解ASP.NET Core MVC 中文件上传等相关内容,仅供学习分享使用。
在实际应用开发中,文件上传是非常常见的功能,文件上传主要分为单文件上传,多文件上传,文件与其他内容混合上传,大文件上传几种情况,本文会分别讲解。
在ASP.NET Core MVC项目中,IFormFile表示使用 HttpRequest 发送的文件,可以用于文件流的接收。将整个文件读入 IFormFile。 IFormFile
是用于处理或保存文件的文件的 C# 表示形式。
文件上传使用的磁盘和内存取决于并发文件上传的数量和大小。 如果应用尝试缓冲过多上传,站点就会在内存或磁盘空间不足时崩溃。 如果文件上传的大小或频率会消耗应用资源,请使用流式传输。
IFormFile的属性和方法如下:
对于小文件的上传,一般采用IFormFile;大文件上传,采用流式上传,实现可靠稳定传输。
单文件上传功能主要分为两部分:文件上传视图和后台处理方法。
首先创建视图,用于单个文件上传。关于视图有两点说明,如下所示:
视图代码如下所示:
form提交后台处理方法OneFileUpload,关于处理方法有几点说明,如下所示:
上传处理代码,如下所示:
///
/// 单文件上传
///
///
public IActionResult OneFileUpload(IFormFile file)
{
var path = Path.Combine(_webHostEnvironment.ContentRootPath, "uploads", string.Format("{0}_{1}", DateTime.Now.Ticks, file.FileName));
using (FileStream fs = new FileStream(path, FileMode.Create))
{
file.CopyTo(fs);
}
return Ok("上传成功");
}
多文件上传表示一次可以上传多个文件。。上传功能主要分为两部分:文件上传视图和后台处理方法。
input控件在类型为file时表示文件上传,默认是单个文件上传,通过设置multiple属性,可实现多文件上传。视图代码如下所示:
多个文件上传,参数为IFormFile数组类型,可以接受上传文件列表,然后循环获取并进行保存即可。如下所示:
///
/// 多文件上传
///
///
public IActionResult MoreFileUpload(IFormFile[] files)
{
foreach (var file in files)
{
var path = Path.Combine(_webHostEnvironment.ContentRootPath, "uploads", string.Format("{0}_{1}", DateTime.Now.Ticks, file.FileName));
using (FileStream fs = new FileStream(path, FileMode.Create))
{
file.CopyTo(fs);
}
}
return Ok("上传成功");
}
在实际应用中,文件上传只是一部分,还需要搭配其他的文本说明,如录入产品信息,并上传附件等。
在Product中,包含两个属性,一个字符串类型的Name,用于绑定名称,一个IFormFile类型的File,用于上传文件。如下所示:
namespace DemoCoreMVC.Models
{
public class Product
{
public string Name { get; set; }
public IFormFile File { get; set; }
}
}
在视图最顶部,为视图指定模型,如下所示:
@model DemoCoreMVC.Models.Product
在form表单中,除了文件上传控件,还有一个文件框,用于输入名称。其中控件name和模型相对应。如下所示:
文件文本混合上传,参数为模型Product,通过属性匹配接收参数,然后获取属性File对应的内存流进行保存即可。如下所示:
///
/// 文件内容混合上传
///
///
public IActionResult FileWithContentUpload(Product product)
{
var file = product.File;
var path = Path.Combine(_webHostEnvironment.ContentRootPath, "uploads", string.Format("{0}_{1}", DateTime.Now.Ticks, file.FileName));
using (FileStream fs = new FileStream(path, FileMode.Create))
{
file.CopyTo(fs);
}
return Ok($"{product.Name} 上传成功");
}
首先如何界定大文件/小文件,并没有统一的标准。根据官网相关参数说明:
MemoryBufferThreshold
充当小型和大型文件之间的边界,这些文件根据应用资源和方案而引发或降低。大文件上传采用流式传输,可降低上传文件时对内存或磁盘空间的需求。
大文件和小文件上传在视图上并无差别,只是后台处理方法不同,如下所示:
首先创建大文件上传帮助类MultipartRequestHelper,如下所示:
using System;
using System.IO;
using Microsoft.Net.Http.Headers;
namespace DemoCoreMVC
{
public static class MultipartRequestHelper
{
// Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
{
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;
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 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)
|| !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
}
// 如果一个section的Header是: Content-Disposition: form-data; name="myfile1"; filename="F:\Misc 002.jpg"
// 那么本方法返回: Misc 002.jpg
public static string GetFileName(ContentDispositionHeaderValue contentDisposition)
{
return Path.GetFileName(contentDisposition.FileName.Value);
}
}
}
处理方法BigFileUploadAsync,关于Action说明,如下所示:
Action中要进行DisableRequestSizeLimit特性说明,否则会有大小限制【InvalidDataException: Multipart body length limit 16384 exceeded】。
在该操作中,使用 MultipartReader
读取窗体的内容,它会读取每个单独的 MultipartSection
,从而根据需要处理文件或存储内容。 读取多部分节后,该操作会执行自己的模型绑定。
///
/// 大文件上传
///
///
[DisableRequestSizeLimit]
public async Task BigFileUploadAsync()
{
var contentType = Request.ContentType;
if (!MultipartRequestHelper.IsMultipartContentType(contentType))
{
ModelState.AddModelError("File",
$"上传文件类型不对.");
return BadRequest(ModelState);
}
var path = Path.Combine(_webHostEnvironment.ContentRootPath, "uploads");
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)
{
var hasContentDispositionHeader =
ContentDispositionHeaderValue.TryParse(
section.ContentDisposition, out var contentDisposition);
if (hasContentDispositionHeader)
{
if (!MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
{
ModelState.AddModelError("File",
$"The request couldn't be processed (Error 2).");
return BadRequest(ModelState);
}
else
{
var fileName = MultipartRequestHelper.GetFileName(contentDisposition);
var loadBufferBytes = 1024;//这个是每一次从Http请求的section中读出文件数据的大小,单位是Byte即字节,这里设置为1024的意思是,每次从Http请求的section数据流中读取出1024字节的数据到服务器内存中,然后写入下面targetFileStream的文件流中,可以根据服务器的内存大小调整这个值。这样就避免了一次加载所有上传文件的数据到服务器内存中,导致服务器崩溃。
using (var targetFileStream = new FileStream(path + "\\" + string.Format("{0}_{1}", DateTime.Now.Ticks, fileName), FileMode.Create, FileAccess.ReadWrite))
{
using (section.Body)
{
//section.Body是System.IO.Stream类型,表示的是Http请求中一个section的数据流,从该数据流中可以读出每一个section的全部数据,所以我们下面也可以不用section.Body.CopyToAsync方法,而是在一个循环中用section.Body.Read方法自己读出数据(如果section.Body.Read方法返回0,表示数据流已经到末尾,数据已经全部都读取完了),再将数据写入到targetFileStream
await section.Body.CopyToAsync(targetFileStream, loadBufferBytes);
}
}
}
}
section = await reader.ReadNextSectionAsync();
}
return Ok("上传成功");
}
注意:在文件上传功能中,上传后的文件一般都要进行重命名的,否则如何客户端上传相同名称的文件,则可能会被覆盖。在本例中,在原文件前面加上了时间戳,以减少重复的概率。
在实际开发中,为了避免客户端上传不满足条件的文件,一般都会进行校验。
为避免文件上传功能造成攻击可能性,常规安全措施如下:
本篇文章主要参考内容如下:
1. 官方文档:https://learn.microsoft.com/zh-cn/aspnet/core/mvc/models/file-uploads?view=aspnetcore-6.0
以上就是ASP.NET Core MVC从入门到精通之文件上传的全部内容。