如何在上传文件时使用HttpClient获取请求正文?

时间:2013-11-04 16:06:16

标签: c# http

我正在使用HttpClient将文件上传到需要Content-MD5标头的API。如何在发送之前从HttpClient获取完整的请求正文,以便我可以对内容运行MD5并在请求标头中使用它?请注意,我还需要在多部分表单数据(即Content-Disposition)和每个部分中的所有其他标头之间包含标头。

我正在使用以下代码,取自this answer

private System.IO.Stream Upload(string url, string param1, Stream fileStream, byte [] fileBytes)
{
    HttpContent stringContent = new StringContent(param1);
    HttpContent fileStreamContent = new StreamContent(fileStream);
    HttpContent bytesContent = new ByteArrayContent(fileBytes);
    using (var client = new HttpClient())
    using (var formData = new MultipartFormDataContent())
    {
        formData.Add(stringContent, "param1", "param1");
        formData.Add(fileStreamContent, "file1", "file1");
        formData.Add(bytesContent, "file2", "file2");
        var response = client.PostAsync(url, formData).Result;
        if (!response.IsSuccessStatusCode)
        {
            return null;
        }
        return response.Content.ReadAsStreamAsync().Result;
    }
}

3 个答案:

答案 0 :(得分:2)

现在,我提前承认我没有测试过这段代码,因为我没有设置测试站点。但是,我已经测试了在LINQPad中发布数据并且代码没有错误并且MD5哈希设置。以下内容适用于您想要做的事情:

private System.IO.Stream Upload(string url, string param1, Stream fileStream, byte[] fileBytes)
{
    HttpContent stringContent = new StringContent(param1);
    HttpContent fileStreamContent = new StreamContent(fileStream);
    HttpContent bytesContent = new ByteArrayContent(fileBytes);

    using (HttpClient client = new HttpClient())
    {
        using (MultipartFormDataContent formData = new MultipartFormDataContent())
        {
            formData.Add(stringContent, "param1", "param1");
            formData.Add(fileStreamContent, "file1", "file1");
            formData.Add(bytesContent, "file2", "file2");

            using (MD5 md5Hash = MD5.Create())
            {
                formData.Headers.ContentMD5 = md5Hash.ComputeHash(formData.ReadAsByteArrayAsync().Result);
            }

            var response = client.PostAsync(url, formData).Result;
            if (!response.IsSuccessStatusCode)
            {
                return null;
            }

            return response.Content.ReadAsStreamAsync().Result;
        }
    }
}

答案 1 :(得分:2)

你的文件有多大? Adam回答的问题是它将文件的全部内容缓冲在内存中。这可能会导致程序因大型文件而耗尽内存,或因磁盘交换过多而性能不佳。

事实上,我发现即使是MultipartFormDataContent.ReadAsStreamAsync()也会将整个内容缓冲到内存中(可能是通过调用MultipartFormDataContent.LoadIntoBufferAsync())。使用StreamContent.ReadAsStreamAsync()时似乎不存在这个问题,所以它似乎是唯一的解决方案,如果遇到内存问题就是编写自己的MultipartFormDataContent实现,它不会缓冲整个内容但是会使用StreamContent.ReadAsStreamAsync()。

注意,我发现MultipartFormDataContent.CopyToAsync()不会缓冲内存中的所有内容,前提是接收流没有。编写一个作为一种管道的Stream实现可能是值得的,其中写入它的任何字节都被md5Hash.ComputeHash(Stream)立即使用。

编辑:这些是我在.NET 4.0上的经验。我听说.NET 4.5在客户端缓冲方面的行为有所不同,所以我不确定MultipartFormDataContent.ReadAsStreamAsync()是如何对它执行的。

答案 2 :(得分:1)

使用HttpClient处理这种类型的一般方法是使用HttpMessageHandler,就像您可能需要做的其他工作一样。添加授权标题,签署消息等。

我还使用基于任务的语法重写了这个,因为它更具有理想性的HttpClient - 调用者可以根据需要调用.Result。

private await Task<System.IO.Stream> Upload(string url, string param1, Stream fileStream, byte[] fileBytes)
{
    HttpContent stringContent = new StringContent(param1);
    HttpContent fileStreamContent = new StreamContent(fileStream);
    HttpContent bytesContent = new ByteArrayContent(fileBytes);

    var handler = new HttpClientHandler();
    var md5Handler = new RequestContentMd5Handler();
    md5Handler.InnerHandler = handler;

    using (HttpClient client = new HttpClient(md5Handler))
    {
        using (MultipartFormDataContent formData = new MultipartFormDataContent())
        {
            formData.Add(stringContent, "param1", "param1");
            formData.Add(fileStreamContent, "file1", "file1");
            formData.Add(bytesContent, "file2", "file2");

            using (var response = await client.PostAsync(url, formData))
            {
                if (!response.IsSuccessStatusCode)
                {
                    return null;
                }

                return await response.Content.ReadAsStreamAsync();
            }
        }
    }
}

此外,在每个请求上重新创建HttpClient通常都是不好的做法(请参阅What is the overhead of creating a new HttpClient per call in a WebAPI client?等),但我在此处留下了与问题风格一致的内容。

这是使用的处理程序......

/// <summary>
/// Handler to assign the MD5 hash value if content is present
/// </summary>
public class RequestContentMd5Handler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        if (request.Content == null)
        {
            return await base.SendAsync(request, cancellationToken);
        }

        await request.Content.AssignMd5Hash();

        var response = await base.SendAsync(request, cancellationToken);

        return response;
    }
}

扩展方法......

    /// <summary>
    /// Compute and assign the MD5 hash of the content.
    /// </summary>
    /// <param name="httpContent"></param>
    /// <returns></returns>
    public static async Task AssignMd5Hash(this HttpContent httpContent)
    {
        var hash = await httpContent.ComputeMd5Hash();

        httpContent.Headers.ContentMD5 = hash;
    }

    /// <summary>
    /// Compute the MD5 hash of the content.
    /// </summary>
    /// <param name="httpContent"></param>
    /// <returns></returns>
    public static async Task<byte[]> ComputeMd5Hash(this HttpContent httpContent)
    {
        using (var md5 = MD5.Create())
        {
            var content = await httpContent.ReadAsStreamAsync();
            var hash = md5.ComputeHash(content);
            return hash;
        }
    }

可以轻松地对各个部件进行单元测试。