在ASP.NET Web API中从控制器返回二进制文件

348

我正在使用ASP.NET MVC的新WebAPI开发一个网络服务,用于提供二进制文件,主要是.cab.exe文件。

以下控制器方法似乎可以正常工作,即它返回一个文件,但它将内容类型设置为application/json

public HttpResponseMessage<Stream> Post(string version, string environment, string filetype)
{
    var path = @"C:\Temp\test.exe";
    var stream = new FileStream(path, FileMode.Open);
    return new HttpResponseMessage<Stream>(stream, new MediaTypeHeaderValue("application/octet-stream"));
}

有更好的方法吗?


4
如果有人想通过Web API和IHTTPActionResult返回一个字节数组流,请参考这里:http://nodogmablog.bryanhogan.net/2017/02/downloading-an-inmemory-file-using-web-api-2/。 - IbrarMumtaz
// 使用 System.IO; // 使用 System.Net.Http; // 使用 System.Net.Http.Headers;public HttpResponseMessage Post(string version, string environment, string filetype) { var path = @"C:\Temp\test.exe"; HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK); var stream = new FileStream(path, FileMode.Open, FileAccess.Read); result.Content = new StreamContent(stream); result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); return result; } - user16559547
8个回答

543

尝试使用一个简单的HttpResponseMessage,并将其Content属性设置为一个StreamContent

// using System.IO;
// using System.Net.Http;
// using System.Net.Http.Headers;

public HttpResponseMessage Post(string version, string environment,
    string filetype)
{
    var path = @"C:\Temp\test.exe";
    HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
    var stream = new FileStream(path, FileMode.Open, FileAccess.Read);
    result.Content = new StreamContent(stream);
    result.Content.Headers.ContentType = 
        new MediaTypeHeaderValue("application/octet-stream");
    return result;
}

关于使用stream,有一些需要注意的事项:

  • 不能调用stream.Dispose()方法,因为Web API在处理控制器方法的result以向客户端发送数据时仍需访问它。因此,不要使用using (var stream = …)块。Web API会代替你处理流的释放。

  • 确保流的当前位置设置为0(即流数据的开头)。在上面的示例中,这是一个明显的条件,因为您刚刚打开了文件。但是,在其他情况下(例如,当您首次将某些二进制数据写入MemoryStream时),请确保使用stream.Seek(0, SeekOrigin.Begin);或设置stream.Position = 0;

  • 对于文件流,明确指定FileAccess.Read权限可以帮助防止Web服务器上的访问权限问题;IIS应用程序池账户通常只被赋予wwwroot的读/列出/执行访问权限。


41
您知道流何时关闭吗?我假设框架最终会调用HttpResponseMessage.Dispose(),而该方法会调用HttpResponseMessage.Content.Dispose(),从而有效地关闭流。 - Steve Guidi
46
史蒂夫 - 你是正确的,我通过在 FileStream.Dispose 中添加断点并运行此代码进行了验证。框架会调用 HttpResponseMessage.Dispose,该方法会调用 StreamContent.Dispose,然后再调用 FileStream.Dispose。 - Dan Gartner
19
由于HttpResponseMessage和流本身在方法外仍会被使用,因此无法真正向结果(HttpResponseMessage)或流本身添加using。正如@Dan所述,当框架完成向客户端发送响应后,它们将被框架处理程序释放。 - carlosfigueira
2
@B.ClayShannon 是的,就是这样。就客户端而言,它只是 HTTP 响应内容中的一堆字节。客户端可以对这些字节进行任何选择,包括将其保存到本地文件中。 - carlosfigueira
5
@carlosfigueira,你好,你知道怎么在所有字节都发送完后删除文件吗? - Zach
显示剩余16条评论

146

对于 Web API 2,您可以实现IHttpActionResult。以下是我的实现:

using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;

class FileResult : IHttpActionResult
{
    private readonly string _filePath;
    private readonly string _contentType;

    public FileResult(string filePath, string contentType = null)
    {
        if (filePath == null) throw new ArgumentNullException("filePath");

        _filePath = filePath;
        _contentType = contentType;
    }

    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        var response = new HttpResponseMessage(HttpStatusCode.OK)
        {
            Content = new StreamContent(File.OpenRead(_filePath))
        };

        var contentType = _contentType ?? MimeMapping.GetMimeMapping(Path.GetExtension(_filePath));
        response.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);

        return Task.FromResult(response);
    }
}

那么在您的控制器中可能会有类似以下的代码:

[Route("Images/{*imagePath}")]
public IHttpActionResult GetImage(string imagePath)
{
    var serverPath = Path.Combine(_rootPath, imagePath);
    var fileInfo = new FileInfo(serverPath);

    return !fileInfo.Exists
        ? (IHttpActionResult) NotFound()
        : new FileResult(fileInfo.FullName);
}

这里有一种方法,可以告诉IIS忽略带有扩展名的请求,以使请求传递到控制器:

<!-- web.config -->
<system.webServer>
  <modules runAllManagedModulesForAllRequests="true"/>

1
好的回答,不是所有的代码都可以在粘贴后立即运行,对于不同的情况(不同的文件)也会有所不同。 - Krzysztof Morcinek
1
@JonyAdamit 谢谢。我认为另一个选项是在方法签名上放置async修饰符并完全删除任务的创建:https://gist.github.com/ronnieoverby/ae0982c7832c531a9022 - Ronnie Overby
4
提醒那些使用IIS7+的人,现在可以省略runAllManagedModulesForAllRequests。具体信息请查看链接:https://www.iis.net/configreference/system.webserver/modules。 - Index
1
@BendEg 看起来我曾经检查过源代码并且它确实如此。而且这是有道理的。由于无法控制框架的源代码,对于这个问题的任何答案都可能随时间而改变。 - Ronnie Overby
2
实际上已经有内置的FileResult类(甚至还有FileStreamResult类)。 - BrainSlugs83
显示剩余12条评论

34

对于使用.NET Core的人:

您可以在API控制器方法中使用IActionResult接口,如下所示。

[HttpGet("GetReportData/{year}")]
public async Task<IActionResult> GetReportData(int year)
{
    // Render Excel document in memory and return as Byte[]
    Byte[] file = await this._reportDao.RenderReportAsExcel(year);

    return File(file, "application/vnd.openxmlformats", "fileName.xlsx");
}

这个例子很简化,但是应该可以表达出要点。在.NET Core中,这个过程比以前的.NET版本要简单得多 - 即不需要设置响应类型、内容、头等等。

当然,文件的MIME类型和扩展名也会根据个人需求而有所不同。

参考资料:@NKosi的Stack Overflow回答


5
注意:如果是一张图片并且希望在浏览器中通过直接URL访问查看,请不要提供文件名。 - Pluto

10

虽然建议的解决方案可行,但还有另一种从控制器返回字节数组的方式,可以正确格式化响应流:

  • 在请求中,设置头部 "Accept: application/octet-stream"。
  • 在服务器端,添加一个支持此MIME类型的媒体类型格式化程序。

不幸的是,WebApi不包括任何针对 "application/octet-stream" 的格式化程序。 这里有一个实现在GitHub上:BinaryMediaTypeFormatter(做了一些小改动使其适用于WebApi 2,方法签名已更改)。

您可以将此格式化程序添加到全局配置中:

HttpConfiguration config;
// ...
config.Formatters.Add(new BinaryMediaTypeFormatter(false));

如果请求正确的 "Accept" header,则 WebApi 现在应使用 BinaryMediaTypeFormatter

我更喜欢这个解决方案,因为操作控制器返回 byte[] 类型更容易测试。尽管另一种解决方案允许您在返回其他内容类型(例如 "image/gif")时拥有更多控制权。


9

如果您在使用接受的答案中的方法下载一个相当大的文件时,遇到API被多次调用的问题,请将响应缓冲设置为true。 System.Web.HttpContext.Current.Response.Buffer = true;

这样可以确保在将二进制内容发送到客户端之前,在服务器端对整个二进制内容进行缓冲。否则,您将看到多个请求发送到控制器,如果您没有正确处理它,文件将变得损坏。


3
"Buffer" 属性已被弃用,推荐使用 "BufferOutput"。默认情况下,其值为 "true"。 - decates

8
您正在使用的重载函数设置序列化格式的枚举。您需要显式指定内容类型,例如:
httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");

4
谢谢您的回复。我已经尝试过这个方法,但在 Fiddler 中仍然看到了 Content Type: application/json。如果在返回 httpResponseMessage 响应之前中断,则似乎已正确设置 Content Type。还有其他的想法吗? - Josh Earl

5

你可以尝试

httpResponseMessage.Content.Headers.Add("Content-Type", "application/octet-stream");

0
您可以尝试以下代码片段。
httpResponseMessage.Content.Headers.Add("Content-Type", "application/octet-stream");

希望它能对你有用。

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