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


323

我正在使用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"));
}

有一个更好的方法吗?


2
任何想要通过Web api和IHTTPActionResult通过流返回字节数组的人,请参见此处:nodogmablog.bryanhogan.net/2017/02/…–
IbrarMumtaz

Answers:


516

尝试使用HttpResponseMessageContent属性设置为的简单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的读取/列出/执行访问权限。


37
您是否会知道何时关闭流?我假设框架最终调用HttpResponseMessage.Dispose(),而后者依次调用HttpResponseMessage.Content.Dispose()有效地关闭了流。
史蒂夫·吉迪

41
史蒂夫-你是对的,我通过在FileStream.Dispose中添加一个断点并运行此代码来进行验证。框架调用HttpResponseMessage.Dispose,后者调用StreamContent.Dispose,后者调用FileStream.Dispose。
丹·加特纳

15
您不能真正using在结果(HttpResponseMessage)或流本身中添加a ,因为它们仍将在方法外使用。正如@Dan所提到的,在将响应发送到客户端之后,它们将被框架处理。
carlosfigueira

2
@ B.ClayShannon是的,就是这样。就客户端而言,HTTP响应的内容只是一堆字节。客户端可以选择任何字节来处理这些字节,包括将其保存到本地文件中。
carlosfigueira

5
@carlosfigueira,您好,您知道在字节全部发送后如何删除文件吗?
扎克2014年

137

对于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
好的答案,并非总是SO代码仅在粘贴之后以及针对不同情况(不同文件)运行。
Krzysztof Morcinek 2014年

1
@JonyAdamit谢谢。我认为另一种选择是async在方法签名上放置一个修饰符,并完全删除任务的创建:gist.github.com/ronnieoverby/ae0982c7832c531a9022
Ronnie Overby

4
对于任何正在运行此IIS7 +的用户,请注意。现在可以省略 runAllManagedModulesForAllRequests 。
索引

1
@BendEg好像一次我检查了源并且确实如此。而且应该这样做。无法控制框架的来源,对此问题的任何答案都可能随时间而改变。
罗尼·欧弗比

1
实际上已经有一个内置的FileResult(甚至FileStreamResult)类。
BrainSlugs83

12

对于使用.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核心这个过程是这样比在.NET之前的版本更加简单-即没有设置响应类型,内容,标题等。

此外,当然,文件和扩展名的MIME类型将取决于个人需求。

参考:@NKosi的SO Post Answer


1
请注意,如果它是图像,并且您希望在可直接访问URL的浏览器中查看它,则不要提供文件名。
冥王星

9

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

  • 在请求中,设置标题“接受:应用程序/八位字节流”。
  • 在服务器端,添加媒体类型格式化程序以支持此mime类型。

不幸的是,WebApi不包含“应用程序/八位字节流”的任何格式程序。在GitHub上有一个实现:BinaryMediaTypeFormatter(有一些小的改动以使其适用于webapi 2,更改了方法签名)。

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

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

BinaryMediaTypeFormatter如果请求指定了正确的Accept标头,则应该使用WebApi 。

我更喜欢这种解决方案,因为操作控制器返回byte []更容易测试。不过,如果您要返回除“ application / octet-stream”(例如“ image / gif”)之外的其他内容类型,则另一种解决方案可以让您更好地控制。


8

对于使用接受的答案中的方法下载相当大的文件时多次调用API的问题的人,请将响应缓冲设置为true System.Web.HttpContext.Current.Response.Buffer = true;

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


3
Buffer物业已被否决赞成BufferOutput。默认为true

6

您正在使用的重载设置了序列化格式化程序的枚举。您需要明确指定内容类型,例如:

httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");

3
谢谢回复。我尝试了这个,但仍在Content Type: application/jsonFiddler中看到。的Content Type,如果我在返回之前打破似乎是正确设置httpResponseMessage响应。还有其他想法吗?
乔什·厄尔

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.