.NET 中的文件上传“源”来如此
.net 中文件处理对象
在 .net
中实现文件上传功能,可以使用 IFormFile
对象,使用分为两类:
- 一种是通过模型绑定的方式而这种方式包含了
IFormFile、List<IFormFile>、IFormFileCollection
三种方式 ; - 另一种是通过
Request.Form.Files
的方式;
这里有一篇文章对该对象做了分析对比,感兴趣的小伙伴请自行查看:
ASP.NET Core
文件上传IFormFile
于Request.Body
的羁绊,https://mp.weixin.qq.com/s/x29umJ5FX5PEqQn_3eVozQ
简单对比
本篇文章的目的是对请求参数集中方式的对比,并模拟实现文件上传功能,Upload2Controller
提供的方法上传到 Upload1Controller
中的 UploadFile5
方法。
创建项目 webapi 项目
使用 dotnet cli
命令创建 webapi
项目,名称为 WebAppUploadFile
:
dotnet new webapi -n WebAppUploadFile --use-minimal-apis false
vs 2022
打开刚创建 WebAppUploadFile
项目,对结构调整如下:
- 删除默认文件
WeatherForecastController.cs
和WeatherForecast.cs
; - 新建文件夹
Models
并添加文件AddGoods.cs
; Program.cs
文件添加HttpClient
;
Program.cs
文件:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddHttpClient(); // 添加 HttpClient
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseAuthorization();
app.MapControllers();
app.Run();
- 模型类
AddGoods.cs
:
说明:该模型用于模拟实际场景中表单数据(有参数和文件)提交。
namespace WebAppUploadFile.Models;
/// <summary>
/// 添加商品
/// </summary>
public sealed class AddGoods
{
/// <summary>
/// 商品ID
/// </summary>
public Guid Id { get; set; } = Guid.NewGuid();
/// <summary>
/// 商品名称
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// 商品零售价
/// </summary>
public decimal RetailPrice { get; set; }
/// <summary>
/// 商品描述
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// 商品图片
/// </summary>
public IFormFile? FormFile { get; set; }
}
- 上传文件
Upload1Controller
using Microsoft.AspNetCore.Mvc;
using WebAppUploadFile.Models;
namespace WebAppUploadFile.Controllers;
/// <summary>
/// 上传文件demo对比
/// </summary>
/// <param name="logger"></param>
[Route("api/[controller]/[action]")]
[ApiController]
public class Upload1Controller(ILogger<Upload1Controller> logger) : ControllerBase
{
/// <summary>
/// 上传单个文件
/// </summary>
/// <param name="formFile"></param>
/// <returns></returns>
[HttpGet]
public string UploadFile1(AddGoods goods) => GetFileInfo(goods.FormFile);
[HttpPost]
public string UploadFile2(AddGoods goods) => GetFileInfo(goods.FormFile);
[HttpPost]
public string UploadFile3([FromQuery] AddGoods goods) => GetFileInfo(goods.FormFile);
[HttpPost]
public string UploadFile4([FromBody] AddGoods goods) => GetFileInfo(goods.FormFile);
[HttpPost]
public string UploadFile5([FromForm] AddGoods goods) => GetFileInfo(goods.FormFile);
/// <summary>
/// 获取文件部分信息
/// </summary>
/// <param name="formFile"></param>
/// <returns></returns>
private string GetFileInfo(IFormFile formFile)
{
string fileInfo = $"{formFile.FileName}--{formFile.Length}--{formFile.ContentDisposition}--{formFile.ContentType}";
logger.LogInformation(fileInfo);
return fileInfo;
}
}
- 上传文件
Upload2Controller
using Microsoft.AspNetCore.Mvc;
using System.Text;
using WebAppUploadFile.Models;
namespace WebAppUploadFile.Controllers;
/// <summary>
/// 上传文件demo对比
/// </summary>
/// <param name="logger"></param>
/// <param name="httpClientFactory"></param>
[Route("api/[controller]/[action]")]
[ApiController]
public class Upload2Controller(ILogger<Upload2Controller> logger, IHttpClientFactory httpClientFactory) : ControllerBase
{
private const string requestUrl = "http://localhost:5000/api/Upload1/UploadFile5";
private readonly HttpClient httpClient = httpClientFactory.CreateClient();
[HttpGet]
public Guid GetGuid() => Guid.NewGuid();
[HttpPost]
public async Task<string> UploadAsync()
{
// 创建 MultipartFormDataContent 对象
var mfdc = new MultipartFormDataContent();
// 将文件添加到 MultipartFormDataContent 对象中
using var fileStream = new FileStream("D:\\test.txt", FileMode.Open, FileAccess.Read);
byte[] bytes = new byte[fileStream.Length];
await fileStream.ReadAsync(bytes, 0, (int)fileStream.Length);
var fileContent = new ByteArrayContent(bytes);
//var fileContent = ToByteArrayContentAsync(fileStream);
mfdc.Add(fileContent, "FormFile", "test.txt");
// 添加其他参数到 MultipartFormDataContent 对象中
mfdc.Add(new StringContent($"{Guid.NewGuid()}", Encoding.UTF8), "Id");
mfdc.Add(new StringContent("zhangsan", Encoding.UTF8), "Name");
mfdc.Add(new StringContent("6.66", Encoding.UTF8), "RetailPrice");
mfdc.Add(new StringContent("商品描述信息", Encoding.UTF8), "Description");
// 发送 POST 请求
using var request = new HttpRequestMessage(HttpMethod.Post, requestUrl);
request.Content = mfdc;
var response = await httpClient.SendAsync(request);
// 处理响应
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadAsStringAsync();
logger.LogInformation("File uploaded successfully!");
return result;
}
else
{
var result = $"Error uploading file: {response.StatusCode}";
logger.LogError(result);
return result;
}
}
[HttpPost]
public async Task<string> AddGoodsAsync([FromForm] AddGoods goods)
{
// 创建 MultipartFormDataContent 对象
var mfdc = new MultipartFormDataContent();
// 1. 将文件添加到 MultipartFormDataContent 对象中
using var fileStream = goods.FormFile!.OpenReadStream();
var fileContent = await ToByteArrayContentAsync(fileStream);
mfdc.Add(fileContent, "FormFile", goods.FormFile.FileName);
// 2. 添加其他参数到 MultipartFormDataContent 对象中
mfdc.Add(new StringContent($"{goods.Id}", Encoding.UTF8), "Id");
mfdc.Add(new StringContent(goods.Name, Encoding.UTF8), "Name");
mfdc.Add(new StringContent($"{goods.RetailPrice}", Encoding.UTF8), "RetailPrice");
mfdc.Add(new StringContent(goods.Description, Encoding.UTF8), "Description");
// 发送 POST 请求
var response = await httpClient.PostAsync(requestUrl, mfdc);
// 处理响应
if (response.IsSuccessStatusCode)
{
var result = await response.Content.ReadAsStringAsync();
logger.LogInformation("File uploaded successfully!");
return result;
}
else
{
var result = $"Error uploading file: {response.StatusCode}";
logger.LogError(result);
return result;
}
}
// Stream 转换 byte[]
private async Task<byte[]> ToByteAsync(Stream stream)
{
byte[] bytes = new byte[stream.Length];
using (var ms = new MemoryStream())
{
await stream.CopyToAsync(ms);
bytes = ms.ToArray();
}
return bytes;
}
// Stream 转换 ByteArrayContent
private async Task<ByteArrayContent> ToByteArrayContentAsync(Stream stream)
{
byte[] bytes = new byte[stream.Length];
await stream.ReadAsync(bytes, 0, (int)stream.Length);
var fileContent = new ByteArrayContent(bytes);
return fileContent;
}
}
温馨提示:针对上面的方法中,MultipartFormDataContent
对象中请求头的 ContentType
不要设置(使用默认设置即可),否则客户端异常(400
);
using System.Net.Http.Headers;
using System.Net.Mime;
...
// 创建 MultipartFormDataContent 对象
var mfdc = new MultipartFormDataContent();
// 自动设置这行代码,客户端会异常(400)
mfdc.Headers.ContentType = new MediaTypeHeaderValue(MediaTypeNames.Multipart.FormData);
测试功能
为了方便模拟测试,我们先把项目 cpoy
一份保存另外一个文件夹中,使用控制台命令分别启动两个项目(注意端口区分)。
运行项目,Swagger
页面显示如下:
Swagger
页面点击操作Upload1Controller
提供的方法;
Action | http 请求方式 | 参数 | curl 命令 | 响应结果 |
---|---|---|---|---|
UploadFile1 | Get | Name=黄瓜&RetailPrice=4.5&Description=本地黄瓜 | curl -X ‘GET’ ‘http://localhost:5124/api/Upload1/UploadFile1?Name=%E9%BB%84%E7%93%9C&RetailPrice=4.5&Description=%E6%9C%AC%E5%9C%B0%E9%BB%84%E7%93%9C’ -H ‘accept: text/plain’ -H ‘Content-Type: multipart/form-data’ -d ‘{“FormFile”: {“name”: “hg.jpg”,“type”: “image/jpeg”}}’ | TypeError: Failed to execute ‘fetch’ on ‘Window’: Request with GET/HEAD method cannot have body. |
UploadFile2 | Post | Name=黄瓜&RetailPrice=4.5&Description=本地黄瓜 | curl -X ‘GET’ ‘http://localhost:5124/api/Upload1/UploadFile1?Name=%E9%BB%84%E7%93%9C&RetailPrice=4.5&Description=%E6%9C%AC%E5%9C%B0%E9%BB%84%E7%93%9C’ -H ‘accept: text/plain’ -H ‘Content-Type: multipart/form-data’ -d ‘{“FormFile”: {“name”: “hg.jpg”,“type”: “image/jpeg”}}’ | hg.jpg–1244257–form-data; name=“FormFile”; filename=“hg.jpg”–image/jpeg |
UploadFile3 | Post | Name=黄瓜&RetailPrice=4.5&Description=本地黄瓜 | curl -X ‘GET’ ‘http://localhost:5124/api/Upload1/UploadFile1?Name=%E9%BB%84%E7%93%9C&RetailPrice=4.5&Description=%E6%9C%AC%E5%9C%B0%E9%BB%84%E7%93%9C’ -H ‘accept: text/plain’ -H ‘Content-Type: multipart/form-data’ -d ‘{“FormFile”: {“name”: “hg.jpg”,“type”: “image/jpeg”}}’ | hg.jpg–1244257–form-data; name=“FormFile”; filename=“hg.jpg”–image/jpeg |
UploadFile4 | Post | 当参数包含IFormFile类型的属性且使用了[FromBody]修饰则无法使用 | — | |
UploadFile5 | Post | {“Id”:“80dc02ac-0a12-4657-899c-05d0b5359d4b”,“Name”:“黄瓜”,“RetailPrice”:4.5,“Description”:“本地黄瓜”,“FormFile”:FileObject} | curl -X ‘POST’ ‘http://localhost:5124/api/Upload1/UploadFile5’ -H ‘accept: text/plain’ -H ‘Content-Type: multipart/form-data’ -F ‘Id=3fa85f64-5717-4562-b3fc-2c963f66afa6’ -F ‘Name=黄瓜’ -F ‘RetailPrice=4.5’ -F ‘Description=本地黄瓜’ -F ‘FormFile=@hg.jpg;type=image/jpeg’ | hg.jpg–1244257–form-data; name=“FormFile”; filename=“hg.jpg”–image/jpeg |
通过上面的测试对比,推荐使用 [FromForm]
修饰的 UploadFile5
为最佳实践。
Swagger
页面点击操作Upload2Controller
提供的方法;
模拟文件上传测试:
Upload2Controller.UploadAsync
上传文件到Upload1Controller.UploadFile5
;
Action | http 请求方式 | 参数 | curl 命令 | 响应结果 |
---|---|---|---|---|
Upload | Post | 无 | curl -X ‘POST’ ‘http://localhost:5000/api/Upload2/Upload’ -H ‘accept: text/plain’ -d ‘’ | hg.jpg–1244257–form-data; name=“FormFile”; filename=“hg.jpg”–image/jpeg |
Upload2Controller.AddGoodsAsync
上传文件到Upload1Controller.UploadFile5
;
Action | http 请求方式 | 参数 | curl 命令 | 响应结果 |
---|---|---|---|---|
AddGoods | Post | {“Id”:“864435f0-33a2-465a-9c65-770a2aae10f5”,“Name”:“黄瓜”,“RetailPrice”:4.5,“Description”:“本地黄瓜”,“FormFile”:FileObject} | curl -X ‘POST’ ‘http://localhost:5000/api/Upload2/Upload’ -H ‘accept: text/plain’ -d ‘’ | hg.jpg–1244257–form-data; name=“FormFile”; filename=“hg.jpg”–image/jpeg |
总结
在 .NET 8
中,实现文件上传功能的方法,主要涉及以下对象:
-
HttpClient
:用于发送HTTP
请求的客户端类,可以创建实例并使用其方法来发送POST
请求。 -
IFormFile
:用来接收上传的文件流对象,可以简单获取文件的基本信息。IFormFile
定义于Microsoft.AspNetCore.Http
命名空间内,代表了从表单上传的文件,比如通过<input type="file">
标签上传的文件。当表单允许多个文件上传时,你可以在控制器方法中使用IFormFile[]
或IList<IFormFile>
来接收多个文件。 -
MultipartFormDataContent
:用于创建包含多个部分(包括文件和文本数据)的表单数据内容。在文件上传场景中,它是必需的,因为它允许你将文件与其他参数一起发送。 -
FileStream
:用于读取本地文件的流。在这个例子中,我们创建一个FileStream
来读取要上传的文件。 -
ByteArrayContent
:用于将字节数组包装成一个HTTP
内容对象。在这里,我们将FileStream
读取的内容转换为字节数组,然后用它来创建ByteArrayContent
对象。 -
StringContent
:用于将字符串包装成一个HTTP
内容对象。在上面的示例中,我们创建了一个StringContent
对象来传递额外的参数。另外需要注意参数格式校验,例如guid
(864435f0-33a2-465a-9c65-770a2aae10f5
),就一定要传入guid
的格式,否则客户端请求会报400
错误。 -
HttpRequestMessage
:是.NET
框架中用于封装HTTP
请求信息的一个类,它位于System.Net.Http
命名空间中。这个类在ASP.NET Web API、WCF REST
服务以及其他需要处理HTTP
请求的场景中广泛使用。HttpRequestMessage
提供了一个模型,用于创建、操作和传递HTTP
请求的所有组成部分,包括请求方法(GET、POST
等)、URL、HTTP
头部、请求内容以及相关属性。
以下是一些
HttpRequestMessage
的关键属性和方法:
Method
:表示HTTP
请求的方法(GET、POST、PUT、DELETE
等)。RequestUri
:表示请求的目标URI
。Headers
:一个HttpRequestHeaders
集合,用于存储请求头信息。Content
:一个HttpContent
对象,用于封装请求体的数据。Properties
:一个字典,用于存储与请求相关的自定义属性。Version
:表示请求的HTTP
版本。GetRequestContext()
:获取与请求相关的上下文信息。CopyToAsync(Stream)
:将请求消息复制到指定的流中。TryGetContentValue< T >()
:尝试从请求内容中获取指定类型的值。
HttpResponseMessage
:当调用PostAsync
方法时,它返回一个HttpResponseMessage
对象,你可以检查它的StatusCode
属性以确定请求是否成功。
接口响应结果为 400
可能的情况:
Content-Type
设置错误,文件上传通常不需要自己手动设置Content-Type
,除非你能确定Content-Type
的值。关于Content-Type
的值可以参考System.Net.Mime.MediaTypeNames
。StringContent
设置额外参数有误,例如参数名称、参数值格式,参数值编码等。参数名称必须与接口名称一致,且必须包含必要参数;参数值格式:必须满足参数规定的格式,例如guid(864435f0-33a2-465a-9c65-770a2aae10f5)
,就一定要传入guid
的格式,否则请求会报400
错误;参数编码,StringContent
可设置参数编码,参数编码格式必须与接口定义编码方式一致,通常为UTF-8