请求.Request.Content.ReadAsMultipartAsync永远不会返回

56
我有一个使用ASP.NET Web Api编写的系统API,现在想扩展它以允许上传图像。我已经做了一些谷歌搜索,发现了使用MultpartMemoryStreamProvider和一些异步方法接受文件的推荐方式,但是我的ReadAsMultipartAsync上的await从未返回。
以下是代码:
[HttpPost]
public async Task<HttpResponseMessage> LowResImage(int id)
{
    if (!Request.Content.IsMimeMultipartContent())
    {
        throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
    }

    var provider = new MultipartMemoryStreamProvider();

    try
    {
        await Request.Content.ReadAsMultipartAsync(provider);

        foreach (var item in provider.Contents)
        {
            if (item.Headers.ContentDisposition.FileName != null)
            {

            }
        }

        return Request.CreateResponse(HttpStatusCode.OK);
    }
    catch (System.Exception e)
    {
        return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, e);
    }
}

我可以一步一步地执行直到:

await Request.Content.ReadAsMultipartAsync(provider);

在此时,它永远不会完成。

为什么我的await从不返回?

更新

我正在尝试使用curl POST到此操作,命令如下:

C:\cURL>curl -i -F filedata=@C:\LowResExample.jpg http://localhost:8000/Api/Photos/89/LowResImage

我也尝试使用以下HTML进行POST操作,但出现了同样的情况:
<form method="POST" action="http://localhost:8000/Api/Photos/89/LowResImage" enctype="multipart/form-data">
    <input type="file" name="fileupload"/>
    <input type="submit" name="submit"/>
</form>

你的客户端代码是什么样子? - svick
你能分享一下你的原始请求是什么样子的吗? - Kiran
在代码中await之后也要设置断点。有时候使用异步/等待时,它不会跳转到下一行(根据我的经验)。 - Micah
1
嗯...我用相同的代码和上面的HTML尝试了一下,但是我没有看到你提到的问题... - Kiran
我来自asp.net web api团队。如果您能发送给我重现项目,我将有兴趣进行更多的调试。我的邮箱是:kiranchalla@hotmail.com - Kiran
5个回答

101

我在 .NET 4.0 中遇到了类似的问题(没有异步/等待)。使用调试器的线程堆栈,我可以确定 ReadAsMultipartAsync 将任务启动到同一线程上,因此会发生死锁。我做了类似于这样的事情:

IEnumerable<HttpContent> parts = null;
Task.Factory
    .StartNew(() => parts = Request.Content.ReadAsMultipartAsync().Result.Contents,
        CancellationToken.None,
        TaskCreationOptions.LongRunning, // guarantees separate thread
        TaskScheduler.Default)
    .Wait();

对我来说,TaskCreationOptions.LongRunning参数非常关键,因为没有它,调用将继续在同一线程上启动任务。您可以尝试使用类似以下的伪代码来查看C# 5.0中是否适用:

await TaskEx.Run(async() => await Request.Content.ReadAsMultipartAsync(provider))

6
干得好!我遇到了这个问题,通过这个方法解决了它。 - guogangj
我正在使用Web API,并且需要在构造函数中调用异步方法。这是我成功实现的唯一方法!谢谢! - Lucio Paiva
10
非常感谢您分享这个信息。有没有人知道为什么这是默认行为,即使它会导致阻塞? - leojh
3
你救了我。我在这上面浪费了大约3天时间。只有一件小事,如果使用自定义的MultipartForDataStreamProvider,你实际上可以更改StartNew操作的内部,使其读取类似于provider = Request.Content.ReadAsMultipartAsync(provider).Result;的内容。然后你可以在Wait函数之后访问这个提供程序。 - dan richardson
2
谢谢!这个对我有用 await Task.FromResult(Request.Content.ReadAsMultipartAsync(provider)); - Schnapz
显示剩余4条评论

11

我在使用所有现代的4.5.2框架时遇到了同样的问题。

我的API方法接受使用POST请求上传的一个或多个文件,这些文件具有多部分内容。它可以处理较小的文件,但是对于大文件,我的方法会一直挂起,因为ReadAsMultipartAsync()函数永远无法完成。

对我有用的方法:使用async控制器方法,并使用await来等待ReadAsMultipartAsync()完成,而不是在同步控制器方法中获取任务结果。

因此,这种方法行不通:

[HttpPost]
public IHttpActionResult PostFiles()
{
    return Ok
    (
        Request.Content.ReadAsMultipartAsync().Result

        .Contents
        .Select(content => ProcessSingleContent(content))
    );
}

private string ProcessSingleContent(HttpContent content)
{
    return SomeLogic(content.ReadAsByteArrayAsync().Result);
}

这样做成功了:

[HttpPost]
public async Task<IHttpActionResult> PostFiles()
{
    return Ok
    (
        await Task.WhenAll
        (
            (await Request.Content.ReadAsMultipartAsync())

            .Contents
            .Select(async content => await ProcessSingleContentAsync(content))  
        )
    );
}

private async Task<string> ProcessSingleContentAsync(HttpContent content)
{
    return SomeLogic(await content.ReadAsByteArrayAsync());
}

其中SomeLogic是一个同步函数,它接受二进制内容并生成一个字符串(可以是任何类型的处理方式)。

更新最后我在这篇文章中找到了解释:https://msdn.microsoft.com/zh-cn/magazine/jj991977.aspx

这种死锁的根本原因在于 await 处理上下文的方式。默认情况下,在等待不完整的任务时,当前“上下文”被捕获并用于在任务完成时恢复方法的执行。这个“上下文”是当前 SynchronizationContext,除非为 null,否则为当前 TaskScheduler。GUI 和 ASP.NET 应用程序都有一个只允许运行一段代码的 SynchronizationContext。当 await 完成时,它尝试在已捕获的上下文中执行异步方法的剩余部分。但此时该上下文已经有一个线程在其中等待异步方法完成。(同步)等待对方完成,导致死锁。

所以,基本上,“Async all the way”的指南有其原因,这是一个很好的例子。


4

通过参考stackoverflow网站上的另一个回答以及一篇关于targetFramework的博客文章,我发现将框架更新至4.5并在web.config文件中添加/更新以下内容可以解决此问题:

<system.web>
    <compilation debug="true" targetFramework="4.5"/>
</system.web>
<appSettings>
    <add key="aspnet:UseTaskFriendlySynchronizationContext" value="true" />
</appSettings>

0

我也遇到了同样的问题。我的解决方案是

public List<string> UploadFiles(HttpFileCollection fileCollection)
    {
        var uploadsDirectoryPath = HttpContext.Current.Server.MapPath("~/Uploads");
        if (!Directory.Exists(uploadsDirectoryPath))
            Directory.CreateDirectory(uploadsDirectoryPath);

        var filePaths = new List<string>();

        for (var index = 0; index < fileCollection.Count; index++)
        {
            var path = Path.Combine(uploadsDirectoryPath, Guid.NewGuid().ToString());
            fileCollection[index].SaveAs(path);
            filePaths.Add(path);
        }

        return filePaths;
    }

并调用

if (!Request.Content.IsMimeMultipartContent())
{
    throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
}

var filePaths = _formsService.UploadFiles(HttpContext.Current.Request.Files);

0

我有一个工作正常的 .Net MVC WebAPi 项目,其中包含以下 Post 方法,似乎运行良好。它与您已经拥有的非常相似,因此这应该会有所帮助。

    [System.Web.Http.AcceptVerbs("Post")]
    [System.Web.Http.HttpPost]
    public Task<HttpResponseMessage> Post()
    {
        // Check if the request contains multipart/form-data.
        if (!Request.Content.IsMimeMultipartContent())
        {
            throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType);
        }
        string fileSaveLocation = @"c:\SaveYourFile\Here\XXX";
        CustomMultipartFormDataStreamProvider provider = new CustomMultipartFormDataStreamProvider(fileSaveLocation);
        Task<HttpResponseMessage> task = Request.Content.ReadAsMultipartAsync(provider).ContinueWith<HttpResponseMessage>(t =>
            {
                if (t.IsFaulted || t.IsCanceled)
                {
                    Request.CreateErrorResponse(HttpStatusCode.InternalServerError, t.Exception);
                }
                foreach (MultipartFileData file in provider.FileData)
                {
                    //Do Work Here
                }
                return Request.CreateResponse(HttpStatusCode.OK);
            }
        );
        return task;
    }

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