Come restituire un file (FileContentResult) in ASP.NET WebAPI


173

In un normale controller MVC, possiamo produrre pdf con a FileContentResult.

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

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

Ma come possiamo cambiarlo in un ApiController?

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

Ecco cosa ho provato ma non sembra funzionare.

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

Il risultato restituito visualizzato nel browser è:

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

E c'è un post simile su SO: Restituzione di file binario dal controller nell'API Web ASP.NET . Parla dell'output di un file esistente. Ma non sono riuscito a farlo funzionare con un flusso.

Eventuali suggerimenti?


1
Questo post mi ha aiutato: stackoverflow.com/a/23768883/585552
Greg

Risposte:


199

Invece di tornare StreamContentcome Content, posso farlo funzionare 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;
}

2
Se la metà superiore risponde alla tua domanda, ti preghiamo di pubblicarla solo come risposta. La seconda parte sembra essere una domanda diversa: pubblica una nuova domanda per questo.
gunr2171,

3
Ciao, grazie per aver condiviso, ho ricevuto una semplice domanda (immagino). Ho un front-end C # che riceve il messaggio httpresponsemessage. Come estraggo il flusso di contenuti e lo rendo disponibile in modo che un utente possa salvarlo su disco o qualcosa del genere (e posso ottenere il file effettivo)? Grazie!
Ronald,

7
Stavo cercando di scaricare un file Excel generato automaticamente. L'uso di stream.GetBuffer () ha sempre restituito un Excel danneggiato. Se invece uso stream.ToArray () il file viene generato senza problemi. Spero che questo aiuti qualcuno.
afnpires,

4
@AlexandrePires Questo perché in MemoryStream.GetBuffer()realtà restituisce il buffer di MemoryStream, che di solito è più grande del contenuto del flusso (per rendere efficienti gli inserimenti). MemoryStream.ToArray()restituisce il buffer troncato alla dimensione del contenuto.
M. Stramm,

19
Per favore, smetti di farlo. Questo tipo di abuso di MemoryStream provoca codice non scalabile e ignora completamente lo scopo di Stream. Pensa: perché invece non è tutto esposto come byte[]buffer? I tuoi utenti possono eseguire facilmente l'applicazione senza memoria.
makhdumi,

97

Se vuoi tornare IHttpActionResultpuoi farlo in questo modo:

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

3
Buon aggiornamento per mostrare il tipo di ritorno IHttpActionResult. Un refactor di questo codice sarebbe quello di spostare una chiamata a un IHttpActionResult personalizzato come quello elencato su: stackoverflow.com/questions/23768596/…
Josh,

Questo post dimostra un'implementazione ordinata monouso ordinata. Nel mio caso, il metodo helper elencato nel link sopra si è rivelato più utile
hanzolo

45

Questa domanda mi ha aiutato.

Quindi, prova questo:

Codice controller:

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

Visualizza markup HTML (con evento click e URL semplice):

<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>

1
Qui stai usando FileStreamper un file esistente sul server. È un po 'diverso da MemoryStream. Ma grazie per l'input.
Blaise,

4
Se leggi da un file su un server Web, assicurati di utilizzare il sovraccarico per FileShare.Read, altrimenti potresti riscontrare eccezioni nel file in uso.
Jeremy Bell,

se lo sostituisci con il flusso di memoria non funzionerà?
Ale,

@JeremyBell è solo un esempio semplificato, qui nessuno parla della produzione e della versione fail-safe.
Aleka,

1
@Blaise Vedi sotto per perché questo codice funziona FileStreamma fallisce MemoryStream. Fondamentalmente ha a che fare con lo Stream Position.
M. Stramm,

9

Ecco un'implementazione che esegue lo streaming del contenuto del file senza memorizzarlo nel buffer (buffering in byte [] / MemoryStream, ecc. Può essere un problema del server se si tratta di un file di grandi dimensioni).

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

Può essere semplicemente usato in questo modo:

public class MyController : ApiController
{
    public IHttpActionResult Get()
    {
        string filePath = GetSomeValidFilePath();
        return new FileResult(filePath);
    }
}

Come elimineresti il ​​file dopo il download? Ci sono hook da notificare al termine del download?
costa

ok, la risposta sembra essere quella di implementare un attributo del filtro azione e rimuovere il file nel metodo OnActionExecuted.
costa

5
Trovato risposta questo post di Risord: stackoverflow.com/questions/2041717/... . Si può usare questa linea var fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.None, 4096, FileOptions.DeleteOnClose);invece diFile.OpenRead(FilePath)
costa

7

Non sono esattamente sicuro di quale parte dare la colpa, ma ecco perché MemoryStreamnon funziona per te:

Mentre scrivi MemoryStream, aumenta la sua Positionproprietà. Il costruttore di StreamContenttiene conto della corrente del flusso Position. Quindi, se scrivi nello stream, quindi lo passi a StreamContent, la risposta inizierà dal nulla alla fine dello stream.

Esistono due modi per risolvere correttamente questo:

1) costruire contenuti, scrivere in streaming

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

2) scrivere in streaming, ripristinare la posizione, costruire contenuti

[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) sembra un po 'meglio se hai un nuovo Stream, 1) è più semplice se il tuo stream non inizia da 0


Questo codice in realtà non fornisce alcuna soluzione al problema, poiché utilizza lo stesso approccio menzionato nella domanda. La domanda afferma già che questo non funziona e posso confermarlo. return Ok (nuovo StreamContent (stream)) restituisce la rappresentazione JSON di StreamContent.
Dmytro Zakharov,

Aggiornato il codice. Questa risposta in realtà risponde alla domanda più sottile di "perché la soluzione semplice funziona con FileStream ma non MemoryStream" piuttosto che come restituire un file in WebApi.
M. Stramm,

3

Per me è stata la differenza tra

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

Il primo stava restituendo la rappresentazione JSON di StringContent: {"Headers": [{"Key": "Content-Type", "Value": ["application / octet-stream; charset = utf-8"]}]}

Mentre il secondo restituiva il file corretto.

Sembra che Request.CreateResponse abbia un sovraccarico che accetta una stringa come secondo parametro e questo sembra essere stato ciò che stava causando il rendering dell'oggetto StringContent stesso come stringa, anziché il contenuto effettivo.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.