Zwracanie pliku binarnego z kontrolera w ASP.NET Web API

323

Pracuję nad usługą internetową przy użyciu nowego interfejsu WebAPI programu ASP.NET MVC, który będzie obsługiwał pliki binarne, głównie .cabi .exepliki.

Następująca metoda kontrolera wydaje się działać, co oznacza, że ​​zwraca plik, ale ustawia typ zawartości na 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"));
}

Czy jest na to lepszy sposób?

Josh Earl
źródło
2
Każdy, kto wyląduje, chcąc zwrócić tablicę bajtów za pośrednictwem strumienia przez interfejs WWW i IHTTPActionResult, zobacz tutaj: nodogmablog.bryanhogan.net/2017/02/…
IbrarMumtaz

Odpowiedzi:

516

Spróbuj użyć prostego HttpResponseMessagez jego Contentwłaściwością ustawioną na 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;
}

Kilka rzeczy do zapamiętania na temat streamużywanych:

  • Nie można dzwonić stream.Dispose(), ponieważ interfejs API sieci Web nadal musi mieć do niego dostęp podczas przetwarzania metody kontrolera w resultcelu wysłania danych z powrotem do klienta. Dlatego nie używaj using (var stream = …)bloku. Interfejs API sieci Web udostępni Ci strumień.

  • Upewnij się, że bieżąca pozycja strumienia ustawiona jest na 0 (tj. Początek danych strumienia). W powyższym przykładzie jest to podane, ponieważ właśnie otworzyłeś plik. Jednak w innych scenariuszach (np. Gdy po raz pierwszy zapisujesz jakieś dane binarne w a MemoryStream), upewnij się stream.Seek(0, SeekOrigin.Begin);lub ustawstream.Position = 0;

  • W przypadku strumieni plików wyraźne określenie FileAccess.Readuprawnień może pomóc w zapobieganiu problemom z prawami dostępu na serwerach internetowych; Kontom puli aplikacji IIS często przyznawane są tylko prawa odczytu / listy / wykonywania dla wwwroot.

carlosfigueira
źródło
37
Czy wiesz, kiedy strumień zostanie zamknięty? Zakładam, że framework ostatecznie wywołuje HttpResponseMessage.Dispose (), który z kolei wywołuje HttpResponseMessage.Content.Dispose () skutecznie zamykając strumień.
Steve Guidi,
41
Steve - masz rację, a ja zweryfikowałem, dodając punkt przerwania do FileStream.Dispose i uruchamiając ten kod. Struktura wywołuje HttpResponseMessage.Dispose, która wywołuje StreamContent.Dispose, która wywołuje FileStream.Dispose.
Dan Gartner
15
Nie można tak naprawdę dodać usingani do wyniku ( HttpResponseMessage), ani do samego strumienia, ponieważ nadal będą one używane poza metodą. Jak wspomniano @Dan, są one usuwane przez środowisko po zakończeniu wysyłania odpowiedzi do klienta.
carlosfigueira
2
@ B.ClayShannon tak, to wszystko. Jeśli chodzi o klienta, to tylko garść bajtów w treści odpowiedzi HTTP. Klient może zrobić z tymi bajtami, co wybierze, w tym zapisać je w pliku lokalnym.
carlosfigueira
5
@carlosfigueira, cześć, czy wiesz, jak usunąć plik po wysłaniu wszystkich bajtów?
Zach.
137

W przypadku interfejsu API sieci Web 2 można zaimplementować IHttpActionResult. To moje:

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

Następnie coś takiego w kontrolerze:

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

A oto jeden ze sposobów, w jaki możesz powiedzieć IIS, aby ignorował żądania z rozszerzeniem, aby żądanie trafiło do kontrolera:

<!-- web.config -->
<system.webServer>
  <modules runAllManagedModulesForAllRequests="true"/>
Ronnie Overby
źródło
1
Dobra odpowiedź, nie zawsze kod SO działa po wklejeniu i dla różnych przypadków (różnych plików).
Krzysztof Morcinek,
1
@JonyAdamit Dzięki. Myślę, że inną opcją jest umieszczenie asyncmodyfikatora w podpisie metody i całkowite usunięcie utworzenia zadania: gist.github.com/ronnieoverby/ae0982c7832c531a9022
Ronnie Overby
4
Tylko heads-up dla każdego, kto przyjdzie z tym działającym IIS7 +. runAllManagedModulesForAllRequests można teraz pominąć .
Indeks,
1
@BendEg Wygląda na to, że kiedyś sprawdziłem źródło i tak się stało. I ma to sens, że powinno. Nie będąc w stanie kontrolować źródła frameworka, każda odpowiedź na to pytanie może z czasem ulec zmianie.
Ronnie Overby,
1
W rzeczywistości jest już wbudowana klasa FileResult (a nawet FileStreamResult).
BrainSlugs83
12

Dla osób korzystających z .NET Core:

Możesz użyć interfejsu IActionResult w metodzie kontrolera API, tak jak ...

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

Ten przykład jest uproszczony, ale powinien być zrozumiały. W .NET Core proces ten jest o wiele prostszy niż w poprzednich wersjach .NET - tzn. Brak ustawienia typu odpowiedzi, treści, nagłówków itp.

Oczywiście typ MIME pliku i rozszerzenia zależy od indywidualnych potrzeb.

Odniesienie: SO Post Answer by @NKosi

KMJungersen
źródło
1
Tylko uwaga, jeśli jest to obraz i chcesz, aby był widoczny w przeglądarce z bezpośrednim dostępem do adresu URL, nie podawaj nazwy pliku.
Pluton
9

Chociaż sugerowane rozwiązanie działa poprawnie, istnieje inny sposób na zwrócenie tablicy bajtów ze sterownika, z odpowiednio sformatowanym strumieniem odpowiedzi:

  • W żądaniu ustaw nagłówek „Zaakceptuj: application / octet-stream”.
  • Po stronie serwera dodaj formater typu nośnika, aby obsługiwać ten typ MIME.

Niestety WebApi nie zawiera żadnego formatyzatora dla „application / octet-stream”. Na GitHub istnieje implementacja: BinaryMediaTypeFormatter (wprowadzono drobne dostosowania, aby działała w przypadku webapi 2, zmieniono podpisy metod).

Możesz dodać ten formatyzator do swojej globalnej konfiguracji:

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

WebApi powinno teraz używać, BinaryMediaTypeFormatterjeśli żądanie określa poprawny nagłówek Accept.

Wolę to rozwiązanie, ponieważ kontroler akcji zwracający bajt [] jest wygodniejszy do przetestowania. Jednak inne rozwiązanie umożliwia większą kontrolę, jeśli chcesz zwrócić inny typ zawartości niż „application / octet-stream” (na przykład „image / gif”).

Eric Boumendil
źródło
8

Dla każdego, kto ma problem z wywołaniem API więcej niż raz podczas pobierania dość dużego pliku przy użyciu metody z zaakceptowanej odpowiedzi, ustaw buforowanie odpowiedzi na true System.Web.HttpContext.Current.Response.Buffer = true;

Dzięki temu cała zawartość binarna jest buforowana po stronie serwera, zanim zostanie wysłana do klienta. W przeciwnym razie zobaczysz wiele żądań wysyłanych do kontrolera, a jeśli nie załatwisz go poprawnie, plik zostanie uszkodzony.

JBA
źródło
3
BufferNieruchomość została zaniechana na korzyść BufferOutput. Domyślnie jest to true.
decyduje
6

Przeciążenie, którego używasz, ustawia wyliczanie formatatorów serializacji. Musisz jawnie określić typ zawartości, np .:

httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
David Peden
źródło
3
Dziękuję za odpowiedź. Wypróbowałem to i wciąż widzę Content Type: application/jsonw Fiddler. Content TypeWydaje się być ustawione prawidłowo, jeśli złamię przed odesłaniem httpResponseMessageodpowiedzi. Jeszcze jakieś pomysły?
Josh Earl
3

Możesz spróbować

httpResponseMessage.Content.Headers.Add("Content-Type", "application/octet-stream");
MickySmig
źródło