Rückgabe der Binärdatei vom Controller in der ASP.NET-Web-API

323

Ich arbeite an einem Webdienst mit der neuen WebAPI von ASP.NET MVC, die hauptsächlich Binärdateien .cabund .exeDateien bereitstellt.

Die folgende Controller-Methode scheint zu funktionieren, dh sie gibt eine Datei zurück, setzt jedoch den Inhaltstyp auf 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"));
}

Gibt es einen besseren Weg, dies zu tun?

Josh Earl
quelle
2
Jeder, der landet und ein Byte-Array per Stream über Web-API und IHTTPActionResult zurückgeben möchte, sieht hier: nodogmablog.bryanhogan.net/2017/02/…
IbrarMumtaz

Antworten:

516

Versuchen Sie es mit einem Simple, HttpResponseMessagedessen ContentEigenschaft auf a gesetzt ist 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;
}

Ein paar Dinge zu beachten über die streamverwendeten:

  • Sie dürfen nicht aufrufen stream.Dispose(), da die Web-API weiterhin darauf zugreifen muss, wenn sie die Controller-Methoden verarbeitet result, um Daten an den Client zurückzusenden. Verwenden Sie daher keinen using (var stream = …)Block. Die Web-API stellt den Stream für Sie bereit.

  • Stellen Sie sicher, dass die aktuelle Position des Streams auf 0 gesetzt ist (dh der Anfang der Daten des Streams). Im obigen Beispiel ist dies eine Selbstverständlichkeit, da Sie die Datei gerade erst geöffnet haben. In anderen Szenarien (z. B. wenn Sie zum ersten Mal einige Binärdaten in a schreiben MemoryStream) müssen Sie dies jedoch sicherstellen stream.Seek(0, SeekOrigin.Begin);oder festlegenstream.Position = 0;

  • Bei Dateistreams kann die explizite Angabe von FileAccess.ReadBerechtigungen dazu beitragen, Probleme mit Zugriffsrechten auf Webservern zu vermeiden. IIS-Anwendungspoolkonten erhalten häufig nur Lese- / Listen- / Ausführungszugriffsrechte für die wwwroot.

carlosfigueira
quelle
37
Würdest du zufällig wissen, wann der Stream geschlossen wird? Ich gehe davon aus, dass das Framework letztendlich HttpResponseMessage.Dispose () aufruft, was wiederum HttpResponseMessage.Content.Dispose () aufruft und den Stream effektiv schließt.
Steve Guidi
41
Steve - Sie haben Recht und ich habe dies überprüft, indem ich FileStream.Dispose einen Haltepunkt hinzugefügt und diesen Code ausgeführt habe. Das Framework ruft HttpResponseMessage.Dispose auf, das StreamContent.Dispose aufruft, das FileStream.Dispose aufruft.
Dan Gartner
15
Sie können usingweder dem Ergebnis ( HttpResponseMessage) noch dem Stream selbst wirklich ein hinzufügen , da diese weiterhin außerhalb der Methode verwendet werden. Wie @Dan erwähnt hat, werden sie vom Framework entsorgt, nachdem die Antwort an den Client gesendet wurde.
Carlosfigueira
2
@ B.ClayShannon ja, das war's auch schon. Für den Client sind es nur ein paar Bytes im Inhalt der HTTP-Antwort. Der Client kann mit diesen Bytes alles tun, was er möchte, einschließlich des Speicherns in einer lokalen Datei.
Carlosfigueira
5
@carlosfigueira, hi, weißt du, wie man die Datei löscht, nachdem alle Bytes gesendet wurden?
Zach
137

Für Web API 2 können Sie implementieren IHttpActionResult. Hier ist meins:

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

Dann so etwas in Ihrem 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);
}

Und hier ist eine Möglichkeit, wie Sie IIS anweisen können, Anforderungen mit einer Erweiterung zu ignorieren, damit die Anforderung an den Controller weitergeleitet wird:

<!-- web.config -->
<system.webServer>
  <modules runAllManagedModulesForAllRequests="true"/>
Ronnie Overby
quelle
1
Gute Antwort, nicht immer wird SO-Code direkt nach dem Einfügen und für verschiedene Fälle (verschiedene Dateien) ausgeführt.
Krzysztof Morcinek
1
@ JonyAdamit Danke. Ich denke, eine andere Option ist, einen asyncModifikator auf die Methodensignatur zu setzen und die Erstellung einer Aufgabe insgesamt zu entfernen: gist.github.com/ronnieoverby/ae0982c7832c531a9022
Ronnie Overby
4
Nur ein Hinweis für alle, die über dieses laufende IIS7 + kommen. runAllManagedModulesForAllRequests können jetzt weggelassen werden .
Index
1
@BendEg Scheint, als hätte ich einmal die Quelle überprüft und es geschah. Und es macht Sinn, dass es sollte. Da die Quelle des Frameworks nicht kontrolliert werden kann, kann sich die Antwort auf diese Frage im Laufe der Zeit ändern.
Ronnie Overby
1
Es gibt bereits eine integrierte FileResult-Klasse (und sogar eine FileStreamResult-Klasse).
BrainSlugs83
12

Für Benutzer von .NET Core:

Sie können die IActionResult-Schnittstelle in einer API-Controller-Methode verwenden, wie z.

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

Dieses Beispiel ist vereinfacht, sollte aber den Punkt vermitteln. In .NET Core ist dieser Prozess so viel einfacher als in früheren Versionen von .NET - dh keine Einstellung von Antworttyp, Inhalt, Headern usw.

Natürlich hängt auch der MIME-Typ für die Datei und die Erweiterung von den individuellen Anforderungen ab.

Referenz: SO Antwort von @NKosi posten

KMJungersen
quelle
1
Nur eine Anmerkung: Wenn es sich um ein Bild handelt und Sie möchten, dass es in einem Browser mit direktem URL-Zugriff angezeigt wird, geben Sie keinen Dateinamen an.
Pluto
9

Während die vorgeschlagene Lösung einwandfrei funktioniert, gibt es eine andere Möglichkeit, ein Byte-Array vom Controller zurückzugeben, wobei der Antwortstrom ordnungsgemäß formatiert ist:

  • Setzen Sie in der Anfrage den Header "Accept: application / octet-stream".
  • Fügen Sie serverseitig einen Medientyp-Formatierer hinzu, um diesen MIME-Typ zu unterstützen.

Leider enthält WebApi keinen Formatierer für "application / octet-stream". Hier auf GitHub gibt es eine Implementierung: BinaryMediaTypeFormatter (es gibt geringfügige Anpassungen, damit es für Webapi 2 funktioniert, Methodensignaturen geändert).

Sie können diesen Formatierer zu Ihrer globalen Konfiguration hinzufügen:

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

WebApi sollte jetzt verwendet werden, BinaryMediaTypeFormatterwenn die Anforderung den richtigen Accept-Header angibt.

Ich bevorzuge diese Lösung, weil ein Action-Controller, der Byte [] zurückgibt, bequemer zu testen ist. Mit der anderen Lösung haben Sie jedoch mehr Kontrolle, wenn Sie einen anderen Inhaltstyp als "application / octet-stream" (z. B. "image / gif") zurückgeben möchten.

Eric Boumendil
quelle
8

Wenn das Problem auftritt, dass die API beim Herunterladen einer relativ großen Datei mit der in der akzeptierten Antwort angegebenen Methode mehrmals aufgerufen wird, setzen Sie die Antwortpufferung auf true. System.Web.HttpContext.Current.Response.Buffer = true;

Dadurch wird sichergestellt, dass der gesamte binäre Inhalt auf der Serverseite gepuffert wird, bevor er an den Client gesendet wird. Andernfalls werden mehrere Anforderungen an den Controller gesendet. Wenn Sie diese nicht ordnungsgemäß verarbeiten, wird die Datei beschädigt.

JBA
quelle
3
Die BufferImmobilie wurde zugunsten von abgelehntBufferOutput . Der Standardwert ist true.
entscheidet
6

Die von Ihnen verwendete Überlastung legt die Aufzählung der Serialisierungsformatierer fest. Sie müssen den Inhaltstyp explizit wie folgt angeben:

httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
David Peden
quelle
3
Danke für die Antwort. Ich habe es ausprobiert und sehe es immer noch Content Type: application/jsonin Fiddler. Das Content Typescheint richtig eingestellt zu sein, wenn ich vor der Rückgabe der httpResponseMessageAntwort eine Pause mache. Noch mehr Ideen?
Josh Earl
3

Du könntest es versuchen

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