Повернення бінарного файлу з контролера у веб-API ASP.NET


323

Я працюю над веб-сервісом, використовуючи новий WebAPI ASP.NET MVC, який обслуговуватиме двійкові файли, переважно .cabта .exeфайли.

Наступний метод контролера, здається, працює, це означає, що він повертає файл, але він встановлює тип вмісту 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"));
}

Чи є кращий спосіб зробити це?


2
Усі, хто приземлився, бажаючи повернути байтовий масив через потік через веб-api та IHTTPActionResult, перегляньте тут: nodogmablog.bryanhogan.net/2017/02/…
IbrarMumtaz

Відповіді:


516

Спробуйте скористатися простим HttpResponseMessageіз Contentвластивістю, встановленою на 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;
}

Кілька речей, які слід зазначити про streamвикористане:

  • Не потрібно телефонувати stream.Dispose(), оскільки веб-API все ще потребує доступу до нього, коли він обробляє метод контролера resultдля повернення даних клієнту. Тому не використовуйте using (var stream = …)блок. Веб-API розпоряджається потоком для вас.

  • Переконайтеся, що для потоку його поточне положення встановлено 0 (тобто початок даних потоку). У наведеному вище прикладі це дані, оскільки ви тільки що відкрили файл. Однак в інших сценаріях (наприклад, коли ви вперше записуєте деякі двійкові дані в a MemoryStream), переконайтесь, що це stream.Seek(0, SeekOrigin.Begin);встановлено або встановленоstream.Position = 0;

  • У потоках файлів, чітко вказавши FileAccess.Readдозвіл, можна запобігти проблемам з правами доступу на веб-серверах; Облікові записи пулу програм IIS часто надаються лише для читання / списку / виконання прав доступу до wwwroot.


37
Чи не знаєте ви, коли потік закривається? Я припускаю, що рамка в кінцевому рахунку викликає HttpResponseMessage.Dispose (), що в свою чергу викликає HttpResponseMessage.Content.Dispose (), ефективно закриваючи потік.
Стів Гуїді

41
Стів - ви правильні, і я перевірив, додавши точку перелому до FileStream.Встановіть і запустіть цей код. Рамка викликає HttpResponseMessage.Dispose, яка викликає StreamContent.Dispose, яка викликає FileStream.Dispose.
Дан Гартнер

15
Ви дійсно не можете додати usingані до результату ( HttpResponseMessage), ані до самого потоку, оскільки вони все ще будуть використовуватись поза методом. Як зазначав @Dan, вони розпоряджаються рамкою після того, як буде зроблено надсилання відповіді клієнту.
carlosfigueira

2
@ B.ClayShannon так, саме про це. Що стосується клієнта, то це лише купа байтів у вмісті HTTP-відповіді. Клієнт може робити з тими байтами все, що вибирає, включаючи збереження його у локальний файл.
carlosfigueira

5
@carlosfigueira, привіт, ти знаєш, як видалити файл після того, як всі байти будуть надіслані?
Зак

137

Для веб-API 2 можна реалізувати IHttpActionResult. Ось моя:

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

Тоді щось подібне у вашому контролері:

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

І ось один спосіб ви можете сказати IIS ігнорувати запити з розширенням, щоб запит перейшов до контролера:

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

1
Приємна відповідь, не завжди код SO працює відразу після вставки та для різних випадків (різних файлів).
Кшиштоф Морчінек

1
@JonyAdamit Дякую Я думаю, що ще один варіант - помістити asyncмодифікатор на підпис методу та повністю видалити створення завдання: gist.github.com/ronnieoverby/ae0982c7832c531a9022
Ronnie Overby

4
Просто голова для тих, хто переходить на цей запущений IIS7 +. runAllManagedModulesForAllRequests тепер можна опустити .
Покажчик

1
@BendEg Схоже, свого часу я перевірив джерело, і це було. І має сенс, що так і повинно. Не маючи можливості контролювати джерело основи, будь-яка відповідь на це питання може змінитися з часом.
Ронні Овербі

1
Фактично вже є вбудований клас FileResult (і навіть FileStreamResult).
BrainSlugs83

12

Для тих, хто використовує .NET Core:

Ви можете використовувати інтерфейс IActionResult в методі контролера API, наприклад ...

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

Цей приклад спрощений, але має бути зрозумілим. У .NET Core цей процес настільки простіший, ніж у попередніх версіях .NET - тобто немає налаштування відповіді типу, вмісту, заголовків тощо.

Також, звичайно, тип MIME для файлу та розширення залежатиме від індивідуальних потреб.

Довідка: SO Відповідь відповіді від @NKosi


1
Просто зауважте, якщо це зображення, і ви хочете, щоб його можна було бачити у веб-переглядачі з прямим доступом до URL-адреси, тоді не вказуйте ім’я файлу.
Плутон

9

Хоча запропоноване рішення працює нормально, є інший спосіб повернути байтовий масив з контролера з правильним форматуванням потоку відповідей:

  • У запиті встановіть заголовок "Прийняти: додаток / октет-потік".
  • Сторінка сервера, додайте формат медіа-типу для підтримки цього типу mime.

На жаль, WebApi не містить жодного форматера для "application / octet-stream". Тут є реалізація на GitHub: BinaryMediaTypeFormatter (є незначні адаптації, щоб вона працювала для webapi 2, підписи методів змінені).

Ви можете додати цей форматер у вашу глобальну конфігурацію:

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

Тепер WebApi слід використовувати, BinaryMediaTypeFormatterякщо запит визначає правильний заголовок Accept.

Я вважаю за краще це рішення, оскільки контролер дій, що повертає байт [], зручніше перевірити. Хоча інше рішення дозволяє вам більше контролювати, якщо ви хочете повернути інший тип вмісту, ніж "application / octet-stream" (наприклад, "image / gif").


8

Для тих, хто має проблему виклику API не раз під час завантаження досить великого файлу, використовуючи метод у прийнятій відповіді, будь ласка, встановіть буферизацію відповіді на справжню System.Web.HttpContext.Current.Response.Buffer = true;

Це гарантує, що весь двійковий вміст буферизований на стороні сервера, перш ніж він буде надісланий клієнту. Інакше ви побачите кілька запитів, що надсилаються до контролера, і якщо ви не будете правильно їх обробляти, файл стане пошкодженим.


3
BufferМайно є застарілим на користь BufferOutput. Це за замовчуванням до true.
декати

6

Перевантаження, яку ви використовуєте, задає перерахування форматів серіалізації. Вам потрібно вказати тип вмісту прямо:

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

3
Дякую за відповідь. Я спробував це, і я все ще бачусь Content Type: application/jsonу Фіддлері. Content Type, Як представляється, встановлений правильно , якщо я порушу до повернення httpResponseMessageвідповіді. Ще ідеї?
Джош граф

3

Ви можете спробувати

httpResponseMessage.Content.Headers.Add("Content-Type", "application/octet-stream");
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.