Retornando arquivo binário do controlador na API da Web do ASP.NET

323

Estou trabalhando em um serviço da Web usando a nova WebAPI do ASP.NET MVC que servirá arquivos binários, principalmente .cabe .exearquivos.

O seguinte método do controlador parece funcionar, o que significa que ele retorna um arquivo, mas está configurando o tipo de conteúdo para 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"));
}

Existe uma maneira melhor de fazer isso?

Josh Earl
fonte
2
Qualquer um que aterra querendo retornar um array de bytes via de fluxo via api web e IHTTPActionResult depois ver aqui: nodogmablog.bryanhogan.net/2017/02/...
IbrarMumtaz

Respostas:

516

Tente usar um simples HttpResponseMessagecom sua Contentpropriedade definida como 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;
}

Algumas coisas a serem observadas sobre o streamusado:

  • Você não deve ligar stream.Dispose(), pois a API da Web ainda precisa acessá-la quando processar o método do controlador resultpara enviar dados de volta ao cliente. Portanto, não use um using (var stream = …)bloco. A API da Web descartará o fluxo para você.

  • Verifique se o fluxo tem sua posição atual definida como 0 (ou seja, o início dos dados do fluxo). No exemplo acima, isso é um dado, pois você acabou de abrir o arquivo. No entanto, em outros cenários (como quando você escreve pela primeira vez alguns dados binários em a MemoryStream), certifique-se de stream.Seek(0, SeekOrigin.Begin);definirstream.Position = 0;

  • Com os fluxos de arquivos, especificar explicitamente a FileAccess.Readpermissão pode ajudar a evitar problemas de direitos de acesso nos servidores da Web; As contas do pool de aplicativos IIS geralmente recebem apenas direitos de leitura / lista / execução de acesso ao wwwroot.

carlosfigueira
fonte
37
Você saberia quando o fluxo é fechado? Estou assumindo que a estrutura finalmente chama HttpResponseMessage.Dispose (), que por sua vez chama HttpResponseMessage.Content.Dispose () efetivamente fechando o fluxo.
23612 Steve
41
Steve - você está correto e eu verifiquei adicionando um ponto de interrupção ao FileStream.Dispose e executando esse código. A estrutura chama HttpResponseMessage.Dispose, que chama StreamContent.Dispose, que chama FileStream.Dispose.
Dan Gartner
15
Você não pode realmente adicionar um usingao resultado ( HttpResponseMessage) ou ao próprio fluxo, pois eles ainda serão usados ​​fora do método. Como o @Dan mencionou, eles são descartados pela estrutura após o envio da resposta ao cliente.
Carlosfigueira 19/06/2013
2
@ B.ClayShannon sim, é isso. Para o cliente, são apenas alguns bytes no conteúdo da resposta HTTP. O cliente pode fazer com esses bytes o que eles escolherem, incluindo salvá-lo em um arquivo local.
Carlosfigueira
5
@carlosfigueira, oi, você sabe como excluir o arquivo após o envio dos bytes?
Zach
137

Para a API da Web 2 , você pode implementar IHttpActionResult. Aqui está o meu:

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);
    }
}

Então, algo como isso no seu controlador:

[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);
}

E aqui está uma maneira de dizer ao IIS para ignorar solicitações com uma extensão, para que a solicitação chegue ao controlador:

<!-- web.config -->
<system.webServer>
  <modules runAllManagedModulesForAllRequests="true"/>
Ronnie Overby
fonte
1
Boa resposta, nem sempre o código SO é executado logo após a colagem e para casos diferentes (arquivos diferentes).
Krzysztof Morcinek
1
@JonyAdamit Thanks. Acho que outra opção é colocar um asyncmodificador na assinatura do método e remover a criação de uma tarefa completamente: gist.github.com/ronnieoverby/ae0982c7832c531a9022
Ronnie Overby
4
Apenas um aviso para qualquer um que esteja executando este IIS7 +. runAllManagedModulesForAllRequests agora pode ser omitido .
Index
1
@BendEg Parece que uma vez eu verifiquei a fonte e o fez. E faz sentido que deveria. Não sendo capaz de controlar a fonte da estrutura, qualquer resposta a essa pergunta pode mudar com o tempo.
perfil completo de Ronnie Overby
1
Na verdade, já existe uma classe FileResult (e até FileStreamResult) incorporada.
precisa saber é o seguinte
12

Para aqueles que usam o .NET Core:

Você pode usar a interface IActionResult em um método de controlador de API, assim ...

    [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");
    }

Este exemplo é simplificado, mas deve passar o ponto. Em .NET Núcleo esse processo é por isso muito mais simples do que nas versões anteriores do .NET - ou seja, nenhum tipo de resposta definição, conteúdo, cabeçalhos, etc.

Além disso, é claro que o tipo MIME do arquivo e a extensão dependerão das necessidades individuais.

Referência: SO Postar resposta por @NKosi

KMJungersen
fonte
1
Apenas uma observação: se é uma imagem e você deseja que ela seja visível em um navegador com acesso direto à URL, não forneça um nome de arquivo.
Plutão
9

Enquanto a solução sugerida funciona bem, há outra maneira de retornar uma matriz de bytes do controlador, com o fluxo de resposta formatado corretamente:

  • Na solicitação, defina o cabeçalho "Accept: application / octet-stream".
  • No servidor, adicione um formatador de tipo de mídia para suportar esse tipo de mímica.

Infelizmente, o WebApi não inclui nenhum formatador para "application / octet-stream". Há uma implementação aqui no GitHub: BinaryMediaTypeFormatter (há pequenas adaptações para fazê-lo funcionar na webapi 2, as assinaturas de método foram alteradas).

Você pode adicionar este formatador à sua configuração global:

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

Agora, o WebApi deve ser usado BinaryMediaTypeFormatterse a solicitação especificar o cabeçalho Aceitar correto.

Eu prefiro esta solução porque um controlador de ação retornando byte [] é mais confortável para testar. No entanto, a outra solução permite maior controle se você deseja retornar outro tipo de conteúdo que não seja "application / octet-stream" (por exemplo, "image / gif").

Eric Boumendil
fonte
8

Para qualquer pessoa que tenha o problema de a API ser chamada mais de uma vez durante o download de um arquivo bastante grande usando o método na resposta aceita, defina o buffer de resposta como true System.Web.HttpContext.Current.Response.Buffer = true;

Isso garante que todo o conteúdo binário seja armazenado em buffer no servidor antes de ser enviado ao cliente. Caso contrário, você verá várias solicitações sendo enviadas ao controlador e, se não tratá-las adequadamente, o arquivo ficará corrompido.

JBA
fonte
3
A Bufferpropriedade foi preterida em favor de BufferOutput. O padrão é true.
decates
6

A sobrecarga que você está usando define a enumeração dos formatadores de serialização. Você precisa especificar o tipo de conteúdo explicitamente, como:

httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
David Peden
fonte
3
Obrigado pela resposta. Eu tentei isso e ainda estou vendo Content Type: application/jsonno Fiddler. O Content Typeparece estar definida corretamente se eu quebrar antes de retornar a httpResponseMessageresposta. Mais alguma ideia?
Josh Earl
3

Você poderia tentar

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