震惊!!!.net 8 文件上传“源”来还可以这么玩!

.net 中文件处理对象

.net 中实现文件上传功能,可以使用 IFormFile 对象,使用分为两类:

  • 一种是通过模型绑定的方式而这种方式包含了 IFormFile、List<IFormFile>、IFormFileCollection 三种方式 ;
  • 另一种是通过 Request.Form.Files 的方式;

这里有一篇文章对该对象做了分析对比,感兴趣的小伙伴请自行查看:

  • ASP.NET Core 文件上传 IFormFileRequest.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 项目,对结构调整如下:

  1. 删除默认文件 WeatherForecastController.csWeatherForecast.cs
  2. 新建文件夹 Models 并添加文件 AddGoods.cs
  3. 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 页面显示如下:

WebAppUploadFile

  • Swagger 页面点击操作 Upload1Controller 提供的方法;
Actionhttp 请求方式参数curl 命令响应结果
UploadFile1GetName=黄瓜&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.
UploadFile2PostName=黄瓜&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
UploadFile3PostName=黄瓜&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
UploadFile4Post当参数包含IFormFile类型的属性且使用了[FromBody]修饰则无法使用
UploadFile5Post{“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 提供的方法;

模拟文件上传测试:

  1. Upload2Controller.UploadAsync 上传文件到 Upload1Controller.UploadFile5
Actionhttp 请求方式参数curl 命令响应结果
UploadPostcurl -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
  1. Upload2Controller.AddGoodsAsync上传文件到 Upload1Controller.UploadFile5
Actionhttp 请求方式参数curl 命令响应结果
AddGoodsPost{“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 对象来传递额外的参数。另外需要注意参数格式校验,例如 guid864435f0-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
  • 15
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ChaITSimpleLove

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值