Restituzione di file binario dal controller nell'API Web ASP.NET


323

Sto lavorando a un servizio Web utilizzando la nuova WebAPI di ASP.NET MVC che servirà per i file binari, principalmente .cabe .exefile.

Il seguente metodo del controller sembra funzionare, nel senso che restituisce un file, ma sta impostando il tipo di contenuto su 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"));
}

C'è un modo migliore per farlo?


2
Chiunque atterrasse desiderando restituire un array di byte tramite flusso tramite API Web e IHTTPActionResult, vedere qui: nodogmablog.bryanhogan.net/2017/02/…
IbrarMumtaz

Risposte:


516

Prova a utilizzare un semplice HttpResponseMessagecon la Contentproprietà impostata su a 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;
}

Alcune cose da notare streamsull'usato:

  • Non è necessario chiamare stream.Dispose(), poiché l'API Web deve essere ancora in grado di accedervi quando elabora i metodi del controller resultper inviare i dati al client. Pertanto, non utilizzare un using (var stream = …)blocco. L'API Web disporrà lo stream per te.

  • Assicurati che la posizione corrente dello stream sia impostata su 0 (ovvero l'inizio dei dati dello stream). Nell'esempio sopra, questo è un dato dato che hai appena aperto il file. Tuttavia, in altri scenari (come quando si scrivono per la prima volta alcuni dati binari in a MemoryStream), accertarsi stream.Seek(0, SeekOrigin.Begin);o impostarestream.Position = 0;

  • Con i flussi di file, specificare esplicitamente l' FileAccess.Readautorizzazione può aiutare a prevenire problemi di diritti di accesso sui server Web; Gli account del pool di applicazioni IIS sono spesso dati solo diritti di lettura / elenco / esecuzione al wwwroot.


37
Ti capita di sapere quando lo stream viene chiuso? Presumo che il framework alla fine chiami HttpResponseMessage.Dispose (), che a sua volta chiama HttpResponseMessage.Content.Dispose () chiudendo efficacemente il flusso.
Steve Guidi,

41
Steve: hai ragione e l'ho verificato aggiungendo un breakpoint a FileStream.Dispose ed eseguendo questo codice. Il framework chiama HttpResponseMessage.Dispose, che chiama StreamContent.Dispose, che chiama FileStream.Dispose.
Dan Gartner,

15
Non puoi davvero aggiungere usinga né il risultato ( HttpResponseMessage) o il flusso stesso, poiché verranno comunque utilizzati al di fuori del metodo. Come accennato da @ Dan, vengono eliminati dal framework dopo aver inviato la risposta al client.
carlosfigueira,

2
@ B.ClayShannon sì, questo è tutto. Per quanto riguarda il client, è solo un mucchio di byte nel contenuto della risposta HTTP. Il client può fare con quei byte qualunque cosa scelga, incluso salvarlo in un file locale.
carlosfigueira,

5
@carlosfigueira, ciao, sai come eliminare il file dopo che tutti i byte sono stati inviati?
Zach,

137

Per l' API Web 2 , è possibile implementare IHttpActionResult. Ecco il mio:

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

Quindi qualcosa del genere nel tuo controller:

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

Ed ecco un modo in cui puoi dire a IIS di ignorare le richieste con un'estensione in modo che la richiesta arrivi al controller:

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

1
Bella risposta, non sempre il codice SO viene eseguito subito dopo l'incollamento e per casi diversi (file diversi).
Krzysztof Morcinek,

1
@JonyAdamit Grazie. Penso che un'altra opzione sia quella di posizionare un asyncmodificatore sulla firma del metodo e rimuovere del tutto la creazione di un'attività: gist.github.com/ronnieoverby/ae0982c7832c531a9022
Ronnie Overby

4
Solo un avvertimento per chiunque si avvicini a questo IIS7 + in esecuzione. runAllManagedModulesForAllRequests ora può essere omesso .
Indice

1
@BendEg Sembra che una volta ho controllato la fonte e lo ha fatto. E ha senso che dovrebbe. Non potendo controllare l'origine del framework, qualsiasi risposta a questa domanda potrebbe cambiare nel tempo.
Ronnie Overby,

1
In realtà esiste già una classe FileResult (e persino FileStreamResult) integrata.
BrainSlugs83

12

Per coloro che utilizzano .NET Core:

Puoi utilizzare l'interfaccia IActionResult in un metodo controller API, in questo modo ...

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

Questo esempio è semplificato, ma dovrebbe essere chiaro. In .NET Nucleo questo processo è così molto più semplice rispetto alle versioni precedenti di .NET - cioè nessun tipo di risposta impostazione, il contenuto, le intestazioni, etc.

Inoltre, ovviamente il tipo MIME per il file e l'estensione dipenderà dalle esigenze individuali.

Riferimento: SO Posta risposta di @NKosi


1
Solo una nota, se è un'immagine e vuoi che sia visualizzabile in un browser con accesso diretto all'URL, non fornire un nome file.
Plutone

9

Sebbene la soluzione suggerita funzioni correttamente, esiste un altro modo per restituire un array di byte dal controller, con il flusso di risposta formattato correttamente:

  • Nella richiesta, imposta l'intestazione "Accetta: application / octet-stream".
  • Sul lato server, aggiungi un formattatore del tipo di supporto per supportare questo tipo MIME.

Sfortunatamente, WebApi non include alcun formattatore per "application / octet-stream". C'è un'implementazione qui su GitHub: BinaryMediaTypeFormatter (ci sono adattamenti minori per farlo funzionare per webapi 2, le firme dei metodi sono cambiate).

Puoi aggiungere questo formattatore nella tua configurazione globale:

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

WebApi dovrebbe ora essere utilizzato BinaryMediaTypeFormatterse la richiesta specifica l'intestazione Accept corretta.

Preferisco questa soluzione perché un controller di azione che restituisce byte [] è più comodo da testare. Tuttavia, l'altra soluzione consente un maggiore controllo se si desidera restituire un altro tipo di contenuto rispetto a "application / octet-stream" (ad esempio "image / gif").


8

Per chiunque abbia il problema che l'API venga chiamato più di una volta durante il download di un file abbastanza grande usando il metodo nella risposta accettata, si prega di impostare il buffering di risposta su vero System.Web.HttpContext.Current.Response.Buffer = true;

Questo si assicura che l'intero contenuto binario sia bufferizzato sul lato server prima di essere inviato al client. Altrimenti vedrai più richieste inviate al controller e se non le gestisci correttamente, il file si corromperà.


3
La Bufferproprietà è stata deprecata a favore di BufferOutput. L'impostazione predefinita è true.
decide il

6

Il sovraccarico che stai utilizzando imposta l'enumerazione dei formattatori di serializzazione. Devi specificare il tipo di contenuto in modo esplicito come:

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

3
Grazie per la risposta. L'ho provato e sto ancora vedendo Content Type: application/jsonin Fiddler. L' Content Typesembra essere impostato correttamente se rompo prima di tornare alla httpResponseMessagerisposta. Altre idee?
Josh Earl

3

Potresti provare

httpResponseMessage.Content.Headers.Add("Content-Type", "application/octet-stream");
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.