Como retornar um arquivo (FileContentResult) no ASP.NET WebAPI

173

Em um controlador MVC comum, podemos produzir pdf com a FileContentResult.

public FileContentResult Test(TestViewModel vm)
{
    var stream = new MemoryStream();
    //... add content to the stream.

    return File(stream.GetBuffer(), "application/pdf", "test.pdf");
}

Mas como podemos transformá-lo em um ApiController?

[HttpPost]
public IHttpActionResult Test(TestViewModel vm)
{
     //...
     return Ok(pdfOutput);
}

Aqui está o que eu tentei, mas parece não funcionar.

[HttpGet]
public IHttpActionResult Test()
{
    var stream = new MemoryStream();
    //...
    var content = new StreamContent(stream);
    content.Headers.ContentType = new MediaTypeHeaderValue("application/pdf");
    content.Headers.ContentLength = stream.GetBuffer().Length;
    return Ok(content);            
}

O resultado retornado exibido no navegador é:

{"Headers":[{"Key":"Content-Type","Value":["application/pdf"]},{"Key":"Content-Length","Value":["152844"]}]}

E há uma postagem semelhante no SO: retornando arquivo binário do controlador na API da Web do ASP.NET . Ele fala sobre a saída de um arquivo existente. Mas não consegui fazê-lo funcionar com um fluxo.

Alguma sugestão?

Blaise
fonte
1
Este post me ajudou: stackoverflow.com/a/23768883/585552
Greg

Respostas:

199

Em vez de retornar StreamContentcomo Content, posso fazê-lo funcionar ByteArrayContent.

[HttpGet]
public HttpResponseMessage Generate()
{
    var stream = new MemoryStream();
    // processing the stream.

    var result = new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = new ByteArrayContent(stream.ToArray())
    };
    result.Content.Headers.ContentDisposition =
        new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
    {
        FileName = "CertificationCard.pdf"
    };
    result.Content.Headers.ContentType =
        new MediaTypeHeaderValue("application/octet-stream");

    return result;
}
Blaise
fonte
2
Se a metade superior responder à sua pergunta, poste-a apenas como resposta. A segunda metade parece ser uma pergunta diferente - publique uma nova pergunta para isso.
precisa saber é o seguinte
3
Oi, obrigado por compartilhar, tenho uma pergunta simples (eu acho). Eu tenho um front end em C # que recebe a mensagem responseprespons. Como extraio o conteúdo do fluxo e o disponibilizo para que um usuário possa salvá-lo em disco ou algo assim (e eu posso obter o arquivo real)? Obrigado!
Ronald
7
Eu estava tentando baixar um arquivo excel auto-gerado. Usar o stream.GetBuffer () sempre retornava um excel corrompido. Se, em vez disso, eu usar stream.ToArray (), o arquivo será gerado sem problemas. Espero que isso ajude alguém.
precisa saber é
4
@AlexandrePires Isso porque, MemoryStream.GetBuffer()na verdade, retorna o buffer do MemoryStream, que geralmente é maior que o conteúdo do fluxo (para tornar as inserções eficientes). MemoryStream.ToArray()retorna o buffer truncado para o tamanho do conteúdo.
M.Stramm
19
Por favor, pare de fazer isso. Esse tipo de abuso do MemoryStream causa um código não escalonável e ignora completamente o objetivo do Streams. Pense: por que tudo não é exposto apenas como byte[]buffers? Seus usuários podem facilmente executar seu aplicativo sem memória.
Makhdumi #
97

Se você quiser retornar, IHttpActionResultfaça o seguinte:

[HttpGet]
public IHttpActionResult Test()
{
    var stream = new MemoryStream();

    var result = new HttpResponseMessage(HttpStatusCode.OK)
    {
        Content = new ByteArrayContent(stream.GetBuffer())
    };
    result.Content.Headers.ContentDisposition = new System.Net.Http.Headers.ContentDispositionHeaderValue("attachment")
    {
        FileName = "test.pdf"
    };
    result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");

    var response = ResponseMessage(result);

    return response;
}
Ogglas
fonte
3
Boa atualização para mostrar o tipo de retorno IHttpActionResult. Um refator desse código seria mover a chamada de um IHttpActionResult personalizado, como o listado em: stackoverflow.com/questions/23768596/…
Josh
Este post demonstra uma implementação organizada e agradável de uso único. No meu caso, o método auxiliar listado no link acima mostrou-se mais útil
hanzolo 29/03
45

Esta pergunta me ajudou.

Então, tente o seguinte:

Código do controlador:

[HttpGet]
public HttpResponseMessage Test()
{
    var path = System.Web.HttpContext.Current.Server.MapPath("~/Content/test.docx");;
    HttpResponseMessage result = new HttpResponseMessage(HttpStatusCode.OK);
    var stream = new FileStream(path, FileMode.Open);
    result.Content = new StreamContent(stream);
    result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment");
    result.Content.Headers.ContentDisposition.FileName = Path.GetFileName(path);
    result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
    result.Content.Headers.ContentLength = stream.Length;
    return result;          
}

Veja a marcação HTML (com evento click e URL simples):

<script type="text/javascript">
    $(document).ready(function () {
        $("#btn").click(function () {
            // httproute = "" - using this to construct proper web api links.
            window.location.href = "@Url.Action("GetFile", "Data", new { httproute = "" })";
        });
    });
</script>


<button id="btn">
    Button text
</button>

<a href=" @Url.Action("GetFile", "Data", new { httproute = "" }) ">Data</a>
aleha
fonte
1
Aqui você está usando FileStreampara um arquivo existente no servidor. É um pouco diferente de MemoryStream. Mas obrigado pela contribuição.
Blaise
4
Se você ler um arquivo em um servidor Web, use a sobrecarga do FileShare.Read; caso contrário, poderá encontrar exceções de arquivo em uso.
Jeremy Bell
se você substituí-lo pelo fluxo de memória, não funcionará?
aleha
@ JeremyBell, é apenas um exemplo simplificado, ninguém fala aqui sobre produção e versão à prova de falhas.
aleha
1
@ Blaise Veja abaixo por que esse código funciona, FileStreammas falha com MemoryStream. Basicamente, tem a ver com o Stream Position.
M.Stramm
9

Aqui está uma implementação que transmite o conteúdo do arquivo sem armazená-lo em buffer (buffer em byte [] / MemoryStream, etc. pode ser um problema no servidor, se for um arquivo grande).

public class FileResult : IHttpActionResult
{
    public FileResult(string filePath)
    {
        if (filePath == null)
            throw new ArgumentNullException(nameof(filePath));

        FilePath = filePath;
    }

    public string FilePath { get; }

    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        var response = new HttpResponseMessage(HttpStatusCode.OK);
        response.Content = new StreamContent(File.OpenRead(FilePath));
        var contentType = MimeMapping.GetMimeMapping(Path.GetExtension(FilePath));
        response.Content.Headers.ContentType = new MediaTypeHeaderValue(contentType);
        return Task.FromResult(response);
    }
}

Pode ser simplesmente usado assim:

public class MyController : ApiController
{
    public IHttpActionResult Get()
    {
        string filePath = GetSomeValidFilePath();
        return new FileResult(filePath);
    }
}
Simon Mourier
fonte
Como você excluiria o arquivo após a conclusão do download? Existem ganchos a serem notificados quando o download terminar?
Costa #
ok, a resposta parece ser implementar um atributo de filtro de ação e remover o arquivo no método OnActionExecuted.
Costa #
5
Encontrei este post Resposta do Risord: stackoverflow.com/questions/2041717/… . Pode-se usar esta linha em var fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.None, 4096, FileOptions.DeleteOnClose);vez deFile.OpenRead(FilePath)
costa
7

Não sei exatamente qual parte culpar, mas eis o porquê MemoryStream que não funciona para você:

Enquanto você escreve MemoryStream, ele aumenta sua Positionpropriedade. O construtor de StreamContentleva em consideração a corrente do fluxo Position. Portanto, se você gravar no fluxo e passá-lo para StreamContent, a resposta começará do nada no final do fluxo.

Há duas maneiras de corrigir corretamente isso:

1) construir conteúdo, escrever para transmitir

[HttpGet]
public HttpResponseMessage Test()
{
    var stream = new MemoryStream();
    var response = Request.CreateResponse(HttpStatusCode.OK);
    response.Content = new StreamContent(stream);
    // ...
    // stream.Write(...);
    // ...
    return response;
}

2) escreva para transmitir, redefinir posição, construir conteúdo

[HttpGet]
public HttpResponseMessage Test()
{
    var stream = new MemoryStream();
    // ...
    // stream.Write(...);
    // ...
    stream.Position = 0;

    var response = Request.CreateResponse(HttpStatusCode.OK);
    response.Content = new StreamContent(stream);
    return response;
}

2) parecer um pouco melhor se você tiver um fluxo novo, 1) será mais simples se o fluxo não iniciar em 0

M.Stramm
fonte
Na verdade, esse código não fornece nenhuma solução para o problema, pois usa a mesma abordagem mencionada na pergunta. A pergunta já afirma que isso não funciona, e posso confirmar isso. return Ok (novo StreamContent (stream)) retorna a representação JSON do StreamContent.
Dmytro Zakharov
Atualizado o código. Esta resposta realmente responde à pergunta mais sutil de 'por que a solução simples funciona com o FileStream, mas não com o MemoryStream', e não como retornar um arquivo no WebApi.
M.Stramm
3

Para mim, foi a diferença entre

var response = Request.CreateResponse(HttpStatusCode.OK, new StringContent(log, System.Text.Encoding.UTF8, "application/octet-stream");

e

var response = Request.CreateResponse(HttpStatusCode.OK);
response.Content = new StringContent(log, System.Text.Encoding.UTF8, "application/octet-stream");

O primeiro retornava a representação JSON de StringContent: {"Headers": [{"Key": "Content-Type", "Value": ["application / octet-stream; charset = utf-8"]}]}

Enquanto o segundo estava retornando o arquivo corretamente.

Parece que Request.CreateResponse tem uma sobrecarga que recebe uma string como o segundo parâmetro e parece ter sido o que estava fazendo com que o próprio objeto StringContent fosse renderizado como uma string, em vez do conteúdo real.

EnderWiggin
fonte