Web API: 如何在使用MultipartMemoryStreamProvider时访问多部分表单值?

49

我过去使用MultipartFormDataStreamProvider来处理多部分请求。

由于我希望上传的文件存储在内存中,而不是磁盘文件,因此我已更改代码以使用MultipartMemoryStreamProvider。文件加载似乎运行良好,但我无法再像MultipartFormDataStreamProvider下的provider.FormData一样访问其他表单值。请问有人可以向我展示如何做到这一点吗?

Fiddler捕获的原始请求:

POST http://myserver.com/QCCSvcHost/MIME/RealtimeTrans/ HTTP/1.1
Content-Type: multipart/form-data; boundary="XbCY"
Host: na-w-lxu3
Content-Length: 1470
Expect: 100-continue
Connection: Keep-Alive

--XbCY
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=PayloadType

X12_270_Request_005010X279A1
--XbCY
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=ProcessingMode

RealTime
--XbCY
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=PayloadID

e51d4fae-7dec-11d0-a765-00a0c91e6fa6
--XbCY
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=TimeStamp

2007-08-30T10:20:34Z
--XbCY
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=SenderID

HospitalA
--XbCY
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=ReceiverID

PayerB
--XbCY
Content-Type: text/plain; charset=utf-8
Content-Disposition: form-data; name=CORERuleVersion

2.2.0
--XbCY
Content-Disposition: form-data; name=Payload; filename=276_5010.edi

ISA*00*~SE*16*0001~GE*1*1~IEA*1*191543498~
--XbCY--

我的控制器代码:

string payload = null;
NameValueCollection nvc = null;
string fname = null;
StringBuilder sb = new StringBuilder();
sb.AppendLine();
foreach (StreamContent item in provider.Contents)
{
    fname = item.Headers.ContentDisposition.FileName;
    if (!String.IsNullOrWhiteSpace(fname))
    {
        payload = item.ReadAsStringAsync().Result;
    }
    else
    {
        nvc = item.ReadAsFormDataAsync().Result;
    }
}
2个回答

123

更新于2015年4月28日

您可以基于MultipartFormDataRemoteStreamProvider创建自定义提供程序。
示例:

public class CustomMultipartFormDataProvider : MultipartFormDataRemoteStreamProvider
{
    public override RemoteStreamInfo GetRemoteStream(HttpContent parent, HttpContentHeaders headers)
    {
        return new RemoteStreamInfo(
            remoteStream: new MemoryStream(),
            location: string.Empty,
            fileName: string.Empty);
    }
}

已更新

自定义内存中的MultiaprtFormDataStreamProvider:

public class InMemoryMultipartFormDataStreamProvider : MultipartStreamProvider
{
    private NameValueCollection _formData = new NameValueCollection();
    private List<HttpContent> _fileContents = new List<HttpContent>();

    // Set of indexes of which HttpContents we designate as form data
    private Collection<bool> _isFormData = new Collection<bool>();

    /// <summary>
    /// Gets a <see cref="NameValueCollection"/> of form data passed as part of the multipart form data.
    /// </summary>
    public NameValueCollection FormData
    {
        get { return _formData; }
    }

    /// <summary>
    /// Gets list of <see cref="HttpContent"/>s which contain uploaded files as in-memory representation.
    /// </summary>
    public List<HttpContent> Files
    {
        get { return _fileContents; }
    }

    public override Stream GetStream(HttpContent parent, HttpContentHeaders headers)
    {
        // For form data, Content-Disposition header is a requirement
        ContentDispositionHeaderValue contentDisposition = headers.ContentDisposition;
        if (contentDisposition != null)
        {
            // We will post process this as form data
            _isFormData.Add(String.IsNullOrEmpty(contentDisposition.FileName));

            return new MemoryStream();
        }

        // If no Content-Disposition header was present.
        throw new InvalidOperationException(string.Format("Did not find required '{0}' header field in MIME multipart body part..", "Content-Disposition"));
    }

    /// <summary>
    /// Read the non-file contents as form data.
    /// </summary>
    /// <returns></returns>
    public override async Task ExecutePostProcessingAsync()
    {
        // Find instances of non-file HttpContents and read them asynchronously
        // to get the string content and then add that as form data
        for (int index = 0; index < Contents.Count; index++)
        {
            if (_isFormData[index])
            {
                HttpContent formContent = Contents[index];
                // Extract name from Content-Disposition header. We know from earlier that the header is present.
                ContentDispositionHeaderValue contentDisposition = formContent.Headers.ContentDisposition;
                string formFieldName = UnquoteToken(contentDisposition.Name) ?? String.Empty;

                // Read the contents as string data and add to form data
                string formFieldValue = await formContent.ReadAsStringAsync();
                FormData.Add(formFieldName, formFieldValue);
            }
            else
            {
                _fileContents.Add(Contents[index]);
            }
        }
    }

    /// <summary>
    /// Remove bounding quotes on a token if present
    /// </summary>
    /// <param name="token">Token to unquote.</param>
    /// <returns>Unquoted token.</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;
    }
}

使用方法:

public async Task Post()
{
    if (!Request.Content.IsMimeMultipartContent("form-data"))
    {
        throw new HttpResponseException(HttpStatusCode.BadRequest);
    }

    var provider = await Request.Content.ReadAsMultipartAsync<InMemoryMultipartFormDataStreamProvider>(new InMemoryMultipartFormDataStreamProvider());

    //access form data
    NameValueCollection formData = provider.FormData;

    //access files
    IList<HttpContent> files = provider.Files;

    //Example: reading a file's stream like below
    HttpContent file1 = files[0];
    Stream file1Stream = await file1.ReadAsStreamAsync();
}

1
这个“content”是不是就像我上面提到的那样,也就是内容数组的内容?我问这个问题是因为看起来你试图读取整个请求的内容而不是内部内容。 - Kiran
感谢提供更多信息。看起来我对ReadAsFormDataAsync()的理解是错误的,因为它只适用于mediatype为application/x-www-form-urlencoded的情况。我已经修改了我的代码。请让我知道这是否有效。 - Kiran
1
它工作得非常好,Kiran!非常感谢您的帮助!我只是有点惊讶 Web API 没有使这更方便,并节省我们编写解析代码的麻烦。祝您有美好的一天! - user2434400
2
很高兴能帮上忙!是的,我完全同意。我会向团队提出这个问题。谢谢!顺便说一句,对于上面的帖子多次更新感到抱歉。现在我已经包含了一个完整的样例。 - Kiran
1
很遗憾我没有足够的“声望”点来投票支持这个答案。我强烈建议其他人这样做。非常感谢Kiran! - user2434400
显示剩余11条评论

1

在Kiran的优秀答案基础上,我整理了2015年4月更新的完整答案。似乎WebAPI中至少有一件事情已经发生了改变,这让我最初感到困惑。现在provider.Files不再存在,使用.Content代替。所以,以下是您至少需要做的最小工作以便在未将文件存储到磁盘上即可读取已发布的文件:

步骤1:创建提供程序类

在项目中添加一个文件,用于存放这个类:

public class InMemoryMultipartFormDataProvider : MultipartFormDataRemoteStreamProvider
{
   public override RemoteStreamInfo GetRemoteStream(HttpContent parent, HttpContentHeaders headers)
   {
      return new RemoteStreamInfo(
                remoteStream: new MemoryStream(),
                location: string.Empty,
                fileName: string.Empty);
   }
}

我相信这将把每个文件转换为内存流而不是存储在磁盘上。
第二步:添加一个控制器动作来解析内容并创建流。
在您的控制器中:
[HttpPost]
public async Task<IHttpActionResult> Upload()
{
   // This endpoint only supports multipart form data
   if (!Request.Content.IsMimeMultipartContent("form-data"))
   {
      return StatusCode(HttpStatusCode.UnsupportedMediaType);
   }

   // read the content in a memory stream per file uploaded
   var provider = await Request.Content.ReadAsMultipartAsync<InMemoryMultipartFormDataProvider>(new InMemoryMultipartFormDataProvider());

   // iterate over each file uploaded and do something with the results
   foreach (var fileContents in provider.Contents) {
      processFileAsMemoryStream(await fileContents.ReadAsStreamAsync());
   }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接