Mengembalikan file biner dari pengontrol di ASP.NET Web API

323

Saya sedang mengerjakan layanan web menggunakan WebAPI baru ASP.NET MVC yang akan menyajikan file biner, sebagian besar .cabdan .exefile.

Metode pengontrol berikut tampaknya berfungsi, artinya ia mengembalikan file, tetapi mengatur jenis konten ke 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"));
}

Apakah ada cara yang lebih baik untuk melakukan ini?

Josh Earl
sumber
2
Siapa pun yang ingin mengembalikan array byte melalui stream melalui web api dan IHTTPActionResult kemudian lihat di sini: nodogmablog.bryanhogan.net/2017/02/…
IbrarMumtaz

Jawaban:

516

Coba gunakan yang sederhana HttpResponseMessagedengan Contentpropertinya disetel ke 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;
}

Beberapa hal yang perlu diperhatikan tentang yang streamdigunakan:

  • Anda tidak boleh menelepon stream.Dispose(), karena Web API masih harus dapat mengaksesnya ketika memproses metode pengontrol resultuntuk mengirim data kembali ke klien. Karena itu, jangan gunakan using (var stream = …)blok. API Web akan membuang aliran untuk Anda.

  • Pastikan bahwa aliran memiliki posisi saat ini diatur ke 0 (yaitu awal dari data aliran). Dalam contoh di atas, ini diberikan karena Anda baru saja membuka file. Namun, dalam skenario lain (seperti ketika Anda pertama kali menulis beberapa data biner ke a MemoryStream), pastikan untuk stream.Seek(0, SeekOrigin.Begin);atau mengaturstream.Position = 0;

  • Dengan aliran file, menentukan FileAccess.Readizin secara eksplisit dapat membantu mencegah masalah hak akses pada server web; Akun kumpulan aplikasi IIS sering hanya diberikan hak baca / daftar / eksekusi ke wwwroot.

carlosfigueira
sumber
37
Apakah Anda akan tahu kapan aliran ditutup? Saya mengasumsikan kerangka kerja akhirnya memanggil HttpResponseMessage.Dispose (), yang pada gilirannya memanggil HttpResponseMessage.Content.Dispose () secara efektif menutup aliran.
Steve Guidi
41
Steve - Anda benar dan saya memverifikasi dengan menambahkan breakpoint ke FileStream. Pilih dan jalankan kode ini. Kerangka kerja ini memanggil HttpResponseMessage.Dispose, yang memanggil StreamContent.Dispose, yang memanggil FileStream.Dispose.
Dan Gartner
15
Anda tidak dapat benar-benar menambahkan usingke salah satu hasil ( HttpResponseMessage) atau aliran itu sendiri, karena mereka masih akan digunakan di luar metode Seperti yang disebutkan oleh @Dan, mereka dibuang oleh framework setelah selesai mengirimkan respons kepada klien.
carlosfigueira
2
@ B.ClayShannon ya, itu saja. Sejauh menyangkut klien itu hanya sekelompok byte dalam konten respon HTTP. Klien dapat melakukan dengan byte tersebut apa pun yang mereka pilih, termasuk menyimpannya ke file lokal.
carlosfigueira
5
@carlosfigueira, hai, apakah Anda tahu cara menghapus file setelah semua byte dikirim?
Zach
137

Untuk Web API 2 , Anda dapat menerapkan IHttpActionResult. Ini milik saya:

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

Maka sesuatu seperti ini di controller Anda:

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

Dan inilah satu cara Anda dapat memberitahu IIS untuk mengabaikan permintaan dengan ekstensi sehingga permintaan akan membuatnya ke controller:

<!-- web.config -->
<system.webServer>
  <modules runAllManagedModulesForAllRequests="true"/>
Ronnie Overby
sumber
1
Jawaban yang bagus, tidak selalu kode SO berjalan setelah menempel dan untuk kasus yang berbeda (file berbeda)
Krzysztof Morcinek
1
@JonyAdamit Terima kasih. Saya pikir pilihan lain adalah menempatkan asyncpengubah pada metode tanda tangan dan menghapus penciptaan tugas sama sekali: gist.github.com/ronnieoverby/ae0982c7832c531a9022
Ronnie Overby
4
Hanya kepala bagi siapa saja yang datang menjalankan IIS7 + ini. runAllManagedModulesForAllRequests sekarang dapat dihilangkan .
Indeks
1
@ BendEg Sepertinya pada suatu waktu saya memeriksa sumbernya dan ternyata benar. Dan masuk akal jika memang begitu. Karena tidak dapat mengendalikan sumber kerangka kerja, jawaban apa pun atas pertanyaan ini dapat berubah seiring waktu.
Ronnie Overby
1
Sebenarnya sudah ada kelas FileResult (dan bahkan FileStreamResult) bawaan.
BrainSlugs83
12

Bagi yang menggunakan .NET Core:

Anda dapat menggunakan antarmuka IActionResult dalam metode pengontrol API, seperti ...

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

Contoh ini disederhanakan, tetapi harus menjelaskan maksudnya. NET Inti proses ini sangat jauh lebih sederhana daripada versi sebelumnya dari NET - yaitu tidak ada jenis respon pengaturan, konten, header, dll

Juga, tentu saja tipe MIME untuk file dan ekstensi akan tergantung pada kebutuhan masing-masing.

Referensi: SO Posting Jawaban oleh @NKosi

KMJungersen
sumber
1
Hanya sebuah catatan, jika itu adalah gambar dan Anda ingin dapat dilihat di browser dengan akses URL langsung, maka jangan berikan nama file.
Pluto
9

Sementara solusi yang disarankan berfungsi dengan baik, ada cara lain untuk mengembalikan array byte dari controller, dengan aliran respons diformat dengan benar:

  • Dalam permintaan, setel tajuk "Terima: aplikasi / octet-stream".
  • Sisi server, tambahkan pemformat jenis media untuk mendukung jenis pantomim ini.

Sayangnya, WebApi tidak menyertakan formatter untuk "application / octet-stream". Ada implementasi di GitHub: BinaryMediaTypeFormatter (ada adaptasi kecil untuk membuatnya berfungsi untuk webapi 2, tanda tangan metode berubah).

Anda dapat menambahkan formatter ini ke konfigurasi global Anda:

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

WebApi sekarang harus digunakan BinaryMediaTypeFormatterjika permintaan menentukan header Terima yang benar.

Saya lebih suka solusi ini karena pengontrol tindakan yang mengembalikan byte [] lebih nyaman untuk diuji. Padahal, solusi lain memungkinkan Anda lebih mengontrol jika Anda ingin mengembalikan tipe konten lain daripada "application / octet-stream" (misalnya "image / gif").

Eric Boumendil
sumber
8

Bagi siapa pun yang memiliki masalah dengan API yang dipanggil lebih dari satu kali saat mengunduh file yang cukup besar menggunakan metode dalam jawaban yang diterima, harap setel buffering respons ke System.Web.HttpContext.Current.Response.Buffer = true;

Ini memastikan bahwa seluruh konten biner disangga di sisi server sebelum dikirim ke klien. Kalau tidak, Anda akan melihat beberapa permintaan dikirim ke controller dan jika Anda tidak menanganinya dengan benar, file tersebut akan rusak.

JBA
sumber
3
The Bufferproperti telah usang dalam mendukung BufferOutput. Standarnya adalah true.
mendeklarasikan
6

Kelebihan yang Anda gunakan menetapkan penghitungan pembuat serialisasi. Anda perlu menentukan jenis konten secara eksplisit seperti:

httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
David Peden
sumber
3
Terima kasih balasannya. Saya mencoba ini, dan saya masih melihat Content Type: application/jsondi Fiddler. The Content Typetampaknya diatur dengan benar jika saya pecah sebelum mengembalikan httpResponseMessagerespon. Ada ide lagi?
Josh Earl
3

Kamu bisa mencoba

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