C#Async ApiController过早关闭OutputStream

时间:2015-02-10 18:39:10

标签: c# asp.net-mvc asp.net-web-api async-await asp.net-web-api2

今天的问题是,当使用WebApi 2和基于Async ApiController的Get方法时,返回文件的内容。当我将Get方法更改为同步时,它可以正常工作,但只要我将其转换回异步,它就会过早地关闭流。 (Fiddler报告连接中止)工作的同步代码是:

 public void Get(int id)
    {
        try
        {
            FileInfo fileInfo = logic.GetFileInfoSync(id);
            HttpResponse response = HttpContext.Current.Response;
            response.Clear();
            response.ClearContent();
            response.Buffer = true;
            response.AddHeader("Content-Disposition", "attachment; filename=\"" + fileInfo.Node.Name + fileInfo.Ext + "\"");
            response.AddHeader("Content-Length", fileInfo.SizeInBytes.ToString());
            response.ContentType = "application/octet-stream";
            logic.GetDownloadStreamSync(id, response.OutputStream);
            response.StatusCode = (int)HttpStatusCode.OK;
            //HttpContext.Current.ApplicationInstance.CompleteRequest();
             response.End();
        }
        catch(Exception ex)
        {
            Console.WriteLine(ex.ToString());
        }
    }

GetDownloadStreamSync如下:

public async Task GetDownloadStream(string fileIdentifier, Stream streamToCopyTo)
{
    string filePath = Path.Combine(fileIdentifierFolder, fileIdentifier);
    using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None, BufferSize, false))
    {
        fs.CopyTo(streamToCopyTo);
    }
 }

--------异步代码----------

Async版本完全相同,只有:

public async Task Get(int id)
{
    FileInfo fileInfo = await logic.GetFileInfoSync(id); // database opp
            HttpResponse response = HttpContext.Current.Response;
            response.Clear();
            response.ClearContent();
            response.Buffer = true;
            response.AddHeader("Content-Disposition", "attachment; filename=\"" + fileInfo.Node.Name + fileInfo.Ext + "\"");
            response.AddHeader("Content-Length", fileInfo.SizeInBytes.ToString());
            response.ContentType = "application/octet-stream";
            await logic.GetDownloadStreamSync(id, response.OutputStream); 
                           //database opp + file I/O
            response.StatusCode = (int)HttpStatusCode.OK;
             //HttpContext.Current.ApplicationInstance.CompleteRequest();
             response.End();
}

使用GetDownloadStream的异步实现,如下所示:(streamToCopyTo是response.OutputStream中的OutputStream)

    public async Task GetDownloadStream(string fileIdentifier, Stream streamToCopyTo)
{
    using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None, BufferSize, true))
    {
        await fs.CopyToAsync(streamToCopyTo);
    }
}

我们试图从前到后采用异步/等待模式,所以希望有人知道为什么会失败?我也尝试过不调用Response.End(),Response.Flush()和HttpContext.Current.ApplicationInstance.CompleteRequest()。另外,在回答下面的问题/评论时,我在response.End()上放置了一个断点,其结果是没有点击GetDownloadStream方法已经完成。也许OutputStream不是异步的?欢迎任何想法! 感谢

**************************最终解决方案******************* ********

非常感谢所有评论过的人,尤其感谢@Noseratio对FileOptions.DeleteOnClose的建议。

[HttpGet]
public async Task<HttpResponseMessage> Get(long id)
{
        HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
        Node node = await logic.GetFileInfoForNodeAsync(id);

        result.Content = new StreamContent(await logic.GetDownloadStreamAsync(id));
        result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
        result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
        {
            FileName = node.Name + node.FileInfo.Extension
        };
        result.Content.Headers.ContentLength = node.FileInfo.SizeInBytes;
        return result
}

GetDownloadStreamAsync如下所示:

 FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None, BufferSize, FileOptions.DeleteOnClose | FileOptions.Asynchronous);

我遗漏了我还在动态解密文件流,这确实有效,所以对于那些感兴趣的人......

FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.None, BufferSize, FileOptions.DeleteOnClose | FileOptions.Asynchronous);
RijndaelManaged rm = new RijndaelManaged();
return new CryptoStream(fs, GetDecryptor(rm, password), CryptoStreamMode.Read);

2 个答案:

答案 0 :(得分:1)

问题的根源实际上在于使用Response.End()。运行Async时,它将在完成文件内容流式传输之前执行Response.End()。使用同步版本时不会出现这种情况,因为在完成文件内容流式传输之后才会调用Response.End()

Response.End()是一种特别糟糕的方式,表示你已完成处理,因为它会抛出TreadAbortException。相反,您应该使用HttpContext.Current.ApplicationInstance.CompleteRequest()

有关详情Response.End, Response.Close, and How Customer Feedback Helps Us Improve MSDN Documentation

,请参阅此文章

答案 1 :(得分:1)

我们需要有一个完整的复制案例才能回答您的确切问题,但我认为您根本不需要async/await。我还认为你应该尽可能避免直接使用HttpContext.Current.Response,尤其是在异步WebAPI控制器方法中。

在这种特殊情况下,您可以使用HttpResponseMessage

[HttpGet]
public HttpResponseMessage Get(int id)
{
    HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
    FileInfo fileInfo = logic.GetFileInfoSync(id);

    FileStream fs = new FileStream(
        filePath, FileMode.Open, FileAccess.Read, FileShare.None, BufferSize, false);

    result.Content = new StreamContent(fs);
    result.Content.Headers.ContentType = 
        new MediaTypeHeaderValue("application/octet-stream");
    result.Content.Headers.ContentDisposition = 
        new ContentDispositionHeaderValue("attachment") 
        {
            FileName = fileInfo.Node.Name + fileInfo.Ext
        };
    result.Content.Headers.ContentLength = fileInfo.SizeInBytes;

    return result;
}

此处没有明确的异步,因此该方法不是async。但是,如果您仍然需要引入一些await,则方法如下:

[HttpGet]
public async Task<HttpResponseMessage> Get(int id)
{
    HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
    // ...
    await fs.CopyToAsync(streamToCopyTo)
    // ...
    return result;
}