Возврат двоичного файла из контроллера в ASP.NET Web API

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
Любой, кто приземлится, желая вернуть байтовый массив через поток через web 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 (то есть начало данных потока). В приведенном выше примере это дано, поскольку вы только что открыли файл. Однако в других случаях (например, когда вы первый написать несколько двоичных данных в MemoryStream), убедитесь , stream.Seek(0, SeekOrigin.Begin);или наборstream.Position = 0;

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

carlosfigueira
источник
37
Вы случайно не знаете, когда поток закроется? Я предполагаю, что инфраструктура в конечном итоге вызывает HttpResponseMessage.Dispose (), которая, в свою очередь, вызывает HttpResponseMessage.Content.Dispose (), эффективно закрывая поток.
Стив Гуиди
41
Стив - вы правы, и я подтвердил, добавив точку останова в FileStream.Dispose и запустив этот код. Фреймворк вызывает HttpResponseMessage.Dispose, который вызывает StreamContent.Dispose, который вызывает FileStream.Dispose.
Дэн Гартнер
15
Вы не можете реально добавить usingни к result ( HttpResponseMessage), ни к самому потоку, так как они все равно будут использоваться вне метода. Как упоминалось @Dan, они удаляются платформой после того, как она отправит ответ клиенту.
Carlosfigueira
2
@ B.ClayShannon да, вот и все. Что касается клиента, это всего лишь несколько байтов в содержимом ответа HTTP. Клиент может делать с этими байтами все, что он выберет, включая сохранение его в локальном файле.
Carlosfigueira
5
@carlosfigueira, привет, вы знаете, как удалить файл после отправки байтов?
Зак
137

Для Web 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
Ронни Оверби,
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 Ядра этот процесс так намного проще , чем в предыдущих версиях .NET - т.е. нет тип ответа установка, содержание, заголовки и т.д.

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

Ссылка: ТАК Сообщение от @NKosi

KMJungersen
источник
1
Просто примечание: если это изображение, и вы хотите, чтобы его можно было просматривать в браузере с прямым доступом по URL, не указывайте имя файла.
Плутон
9

Хотя предлагаемое решение работает нормально, есть другой способ вернуть байтовый массив из контроллера с правильно отформатированным потоком ответов:

  • В запросе установите заголовок «Accept: application / octet-stream».
  • На стороне сервера добавьте средство форматирования мультимедиа для поддержки этого типа MIME.

К сожалению, WebApi не содержит никакого средства форматирования для «application / octet-stream». Здесь на GitHub есть реализация: BinaryMediaTypeFormatter (есть небольшие изменения, чтобы заставить его работать для webapi 2, сигнатуры методов изменены).

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

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

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

Я предпочитаю это решение, потому что контроллер действия, возвращающий byte [], более удобен для тестирования. Тем не менее, другое решение позволяет вам лучше контролировать, если вы хотите вернуть другой тип контента, чем «application / octet-stream» (например, «image / gif»).

Эрик Бумендил
источник
8

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

Это гарантирует, что весь двоичный контент буферизируется на стороне сервера перед его отправкой клиенту. В противном случае вы увидите многократный запрос на контроллер, и если вы не обработаете его должным образом, файл будет поврежден.

JBA
источник
3
BufferИмущество является устаревшим в пользу BufferOutput. По умолчанию оно true.
17
6

Используемая вами перегрузка устанавливает перечисление форматировщиков сериализации. Вы должны указать тип содержимого явно как:

httpResponseMessage.Content.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
Дэвид Педен
источник
3
Спасибо за ответ. Я попробовал это, и я все еще вижу Content Type: application/jsonв Fiddler. Content Type, Как представляется, установлен правильно , если я нарушу до возвращения httpResponseMessageответа. Есть еще идеи?
Джош Эрл
3

Вы могли бы попробовать

httpResponseMessage.Content.Headers.Add("Content-Type", "application/octet-stream");
MickySmig
источник