Jak zwrócić plik (FileContentResult) w ASP.NET WebAPI

173

W zwykłym kontrolerze MVC możemy wyprowadzić plik PDF z rozszerzeniem FileContentResult.

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

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

Ale jak możemy to zmienić w ApiController?

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

Oto, czego próbowałem, ale wydaje się, że nie działa.

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

Zwrócony wynik wyświetlany w przeglądarce to:

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

I jest podobny post na SO: Returning binary file from controller in ASP.NET Web API . Mówi o wyprowadzaniu istniejącego pliku. Ale nie mogłem zmusić tego do pracy ze strumieniem.

Jakieś sugestie?

Blaise
źródło
1
Ten post mi pomógł: stackoverflow.com/a/23768883/585552
Greg

Odpowiedzi:

199

Zamiast wracać StreamContentjako Content, mogę sprawić, że to zadziała 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
źródło
2
Jeśli górna połowa odpowiada na twoje pytanie, zamieść to tylko jako odpowiedź. Druga połowa wydaje się być innym pytaniem - zadaj nowe pytanie.
gunr2171
3
Cześć, dziękuję za udostępnienie, mam proste pytanie (chyba). Mam fronton C #, który odbiera httpresponsemessage. Jak wyodrębnić zawartość strumienia i udostępnić go, aby użytkownik mógł zapisać go na dysku lub w czymś (a ja mogę pobrać rzeczywisty plik)? Dzięki!
Ronald
7
Próbowałem pobrać samodzielnie wygenerowany plik programu Excel. Użycie stream.GetBuffer () zawsze zwracało uszkodzony plik Excel. Jeśli zamiast tego użyję stream.ToArray (), plik zostanie wygenerowany bez problemu. Mam nadzieję, że to komuś pomoże.
afnpires
4
@AlexandrePires Dzieje się tak, ponieważ w MemoryStream.GetBuffer()rzeczywistości zwraca bufor MemoryStream, który jest zwykle większy niż zawartość strumienia (aby wstawienia były wydajne). MemoryStream.ToArray()zwraca bufor obcięty do rozmiaru zawartości.
M.Stramm
19
Proszę, przestań to robić. Ten rodzaj nadużycia MemoryStream powoduje nieskalowalny kod i całkowicie ignoruje cel strumieni. Pomyśl: dlaczego byte[]zamiast tego wszystko nie jest ujawniane jako bufory? Użytkownicy mogą łatwo uruchomić aplikację z powodu braku pamięci.
makhdumi
97

Jeśli chcesz wrócić IHttpActionResult, możesz to zrobić w ten sposób:

[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
źródło
3
Dobra aktualizacja pokazująca typ zwracany IHttpActionResult. Refaktorem tego kodu byłoby wywołanie niestandardowego IHttpActionResult, takiego jak ten wymieniony na stronie: stackoverflow.com/questions/23768596/ ...
Josh
Ten post pokazuje ładną, uporządkowaną implementację jednorazowego użytku. W moim przypadku bardziej pomocna okazała się metoda pomocnicza wymieniona w powyższym linku
hanzolo
45

To pytanie mi pomogło.

Więc spróbuj tego:

Kod kontrolera:

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

Wyświetl znaczniki HTML (ze zdarzeniem kliknięcia i prostym adresem URL):

<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
źródło
1
Tutaj używasz FileStreamistniejącego pliku na serwerze. To trochę różni się od MemoryStream. Ale dzięki za wkład.
Blaise,
4
Jeśli czytasz z pliku na serwerze internetowym, upewnij się, że używasz przeciążenia dla FileShare.Read, w przeciwnym razie możesz napotkać wyjątki w użyciu pliku.
Jeremy Bell
jeśli zastąpisz go strumieniem pamięci to nie zadziała?
aleha
@JeremyBell to tylko uproszczony przykład, nikt nie mówi tutaj o wersji produkcyjnej i bezpiecznej.
aleha
1
@Blaise patrz niżej dlaczego ten kod działa z FileStreamale nie z MemoryStream. Zasadniczo ma to związek ze Streamem Position.
M.Stramm
9

Oto implementacja, która przesyła zawartość pliku na zewnątrz bez buforowania go (buforowanie w bajcie [] / MemoryStream itp. Może być problemem serwera, jeśli jest to duży plik).

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

Można go po prostu użyć w następujący sposób:

public class MyController : ApiController
{
    public IHttpActionResult Get()
    {
        string filePath = GetSomeValidFilePath();
        return new FileResult(filePath);
    }
}
Simon Mourier
źródło
Jak usunąłbyś plik po zakończeniu pobierania? Czy są jakieś haki, które mają być powiadamiane o zakończeniu pobierania?
costa
ok, wydaje się, że odpowiedzią jest zaimplementowanie atrybutu filtru akcji i usunięcie pliku w metodzie OnActionExecuted.
costa
5
Znaleziono ten post Odpowiedź Risorda: stackoverflow.com/questions/2041717/… . Można użyć tej linii var fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.None, 4096, FileOptions.DeleteOnClose);zamiastFile.OpenRead(FilePath)
costa
7

Nie jestem dokładnie pewien, którą część winić, ale oto dlaczego MemoryStreamnie działa w Twoim przypadku:

Kiedy piszesz do MemoryStream, zwiększa swoją Positionwłasność. Konstruktor StreamContentbierze pod uwagę prąd strumienia Position. Więc jeśli napiszesz do strumienia, a następnie przekażesz go do StreamContent, odpowiedź zacznie się od nicości na końcu strumienia.

Istnieją dwa sposoby, aby to naprawić:

1) konstruować treść, pisać do strumienia

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

2) napisz do strumienia, zresetuj pozycję, skonstruuj zawartość

[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) wygląda trochę lepiej, jeśli masz nowy strumień, 1) jest prostszy, jeśli strumień nie zaczyna się od 0

M.Stramm
źródło
Ten kod w rzeczywistości nie zapewnia rozwiązania problemu, ponieważ wykorzystuje to samo podejście, które zostało wspomniane w pytaniu. Pytanie już mówi, że to nie działa i mogę to potwierdzić. return Ok (new StreamContent (stream)) zwraca reprezentację StreamContent w formacie JSON.
Dmytro Zakharov
Zaktualizowano kod. Ta odpowiedź w rzeczywistości odpowiada na bardziej subtelne pytanie „dlaczego proste rozwiązanie działa z FileStream, ale nie MemoryStream”, a nie jak zwrócić plik w WebApi.
M. Stramm
3

Dla mnie to była różnica między

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

i

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

Pierwsza zwróciła reprezentację StringContent w formacie JSON: {"Headers": [{"Key": "Content-Type", "Value": ["application / octet-stream; charset = utf-8"]}]}

Podczas gdy drugi zwracał właściwy plik.

Wygląda na to, że Request.CreateResponse ma przeciążenie, które pobiera ciąg jako drugi parametr i wydaje się, że to właśnie spowodowało, że sam obiekt StringContent był renderowany jako ciąg znaków, zamiast rzeczywistej zawartości.

EnderWiggin
źródło