WebAPI通过multipart/form-data方式接收文件时由开发自行决定如何保存文件

11 篇文章 1 订阅

该篇内容只能在IIS宿主情况下才能取到上传的文件,修改版地址为《MultipartFormDataMemoryStreamProvider修正以支持非IIS宿主的情况

今年4月份写的《WebAPI通过multipart/form-data方式同时上传文件以及数据(含HttpClient上传Demo)》中,有提到用MultipartFormDataStreamProvider来接收文件时,会自动将文件保存至指定目录下,代码中还备注了下有时间研究下如何不直接保存文件,而是直接接收文件流,然后由用户代码指定如何保存,结果一晃快半年了,却没有任何后续,其实自己压根把这事忘了,而且最近上外网多了,发现好多东西老外都已经有了具体实现,只是中文方面缺乏相对应的翻译或者资料而已

先来张UML图

图中相关类为Microsoft默认提供的MIME处理类,最后的MultipartFormDataStreamProvider也在前篇博客中有介绍用法,如果要实现开发自定义如何保存文件,按个人最开始的想法是只要继承MultipartFormDataStreamProvider,并重载相关方法即可,可惜通过ILSpy查看源代码,里面用到的一些关键字段居然都是private或者internal的,怪不得老外都要直接继承MultipartStreamProvider,然后将MultipartFormDataStreamProvider内的相关方法抄了一遍,好吧,我也就一边抄老外,一边抄ILSpy反编译的源代码了,类名什么的也遵循老外起的名字

using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
/// <summary>
/// 与MultipartFormDataStreamProvider对应,但不将文件直接存入指定位置,而是需要自己指定数据流如何保存
/// </summary>
public class MultipartFormDataMemoryStreamProvider : MultipartStreamProvider
{
    private NameValueCollection _formData = new NameValueCollection();
    private Collection<bool> _isFormData = new Collection<bool>();
    /// <summary>
    /// 获取文件对应的HttpContent集合,文件如何读取由实际使用方确定,可以ReadAsByteArrayAsync,也可以ReadAsStreamAsync
    /// </summary>
    public Collection<HttpContent> FileContents
    {
        get
        {
            if (this._isFormData.Count != this.Contents.Count)//两者总数不一致,认为未执行过必须的Request.Content.ReadAsMultipartAsync(provider)方法
            {
                throw new InvalidOperationException("System.Net.Http.HttpContentMultipartExtensions.ReadAsMultipartAsync must be called first!");
            }
            return new Collection<HttpContent>(this.Contents.Where((ct, idx) => !this._isFormData[idx]).ToList());
        }
    }
    /// <summary>Gets a <see cref="T:System.Collections.Specialized.NameValueCollection" /> of form data passed as part of the multipart form data.</summary>
    /// <returns>The <see cref="T:System.Collections.Specialized.NameValueCollection" /> of form data.</returns>
    public NameValueCollection FormData
    {
        get
        {
            return this._formData;
        }
    }
    public override async Task ExecutePostProcessingAsync()
    {
        for (var i = 0; i < this.Contents.Count; i++)
        {
            if (!this._isFormData[i])//非文件
            {
                continue;
            }
            var formContent = this.Contents[i];
            ContentDispositionHeaderValue contentDisposition = formContent.Headers.ContentDisposition;
            string formFieldName = UnquoteToken(contentDisposition.Name) ?? string.Empty;
            string formFieldValue = await formContent.ReadAsStringAsync();
            this.FormData.Add(formFieldName, formFieldValue);
        }
    }
    public override Stream GetStream(HttpContent parent, HttpContentHeaders headers)
    {
        if (parent == null)
        {
            throw new ArgumentNullException("parent");
        }
        if (headers == null)
        {
            throw new ArgumentNullException("headers");
        }
        ContentDispositionHeaderValue contentDisposition = headers.ContentDisposition;
        if (contentDisposition == null)
        {
            throw new InvalidOperationException("Content-Disposition is null");
        }
        this._isFormData.Add(string.IsNullOrEmpty(contentDisposition.FileName));
        return new MemoryStream();
    }
    /// <summary>
    /// 复制自 System.Net.Http.FormattingUtilities 下同名方法,因为该类为internal,不能在其它命名空间下被调用
    /// </summary>
    /// <param name="token"></param>
    /// <returns></returns>
    private static string UnquoteToken(string token)
    {
        if (string.IsNullOrWhiteSpace(token))
        {
            return token;
        }
        if (token.StartsWith("\"", StringComparison.Ordinal) && token.EndsWith("\"", StringComparison.Ordinal) && token.Length > 1)
        {
            return token.Substring(1, token.Length - 2);
        }
        return token;
    }
}
使用方法与前篇博客相同,只需要将MultipartFormDataStreamProvider替换为MultipartFormDataMemoryStreamProvider即可(注:因为如何保存由开发自行指定,所以这里也就不再需要带参构造函数),然后保存部分代码例子如下,这里是用Stream读取的方式来保存至本地
foreach (var fileContent in provider.FileContents)
{
    var stream = await fileContent.ReadAsStreamAsync();
    using (StreamWriter sw = new StreamWriter(Path.Combine(root, fileContent.Headers.ContentDisposition.FileName)))
    {
        stream.CopyTo(sw.BaseStream);
        sw.Flush();
    }
}
然后再补充个如果通过MultipartFileStreamProvider方式来保存文件时,如何修改默认的稀奇古怪的起名方式(BodyPart_加Guid码,该方式同样适用于MultipartFormDataStreamProvider),这里在保存的文件名上添加文件类型后缀
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
public class MultipartFileWithExtensionStreamProvider : MultipartFileStreamProvider
{
    // 摘要: 
    //     初始化 System.Net.Http.MultipartFileStreamProvider 类的新实例。
    //
    // 参数: 
    //   rootPath:
    //     MIME 多部分正文部分的内容写入到的根路径。
    public MultipartFileWithExtensionStreamProvider(string rootPath)
        : this(rootPath, 4096)
    {
    }
    //
    // 摘要: 
    //     初始化 System.Net.Http.MultipartFileStreamProvider 类的新实例。
    //
    // 参数: 
    //   rootPath:
    //     MIME 多部分正文部分的内容写入到的根路径。
    //
    //   bufferSize:
    //     为写入到文件而缓冲的字节数。
    public MultipartFileWithExtensionStreamProvider(string rootPath, int bufferSize)
        : base(rootPath, bufferSize)
    {
    }
    public override string GetLocalFileName(HttpContentHeaders headers)
    {
        return base.GetLocalFileName(headers) + Path.GetExtension(headers.ContentDisposition.FileName);
    }
}
使用方法么还是只要替换对应的StreamProvider即可

  • 6
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值