Как вернуть файл (FileContentResult) в ASP.NET WebAPI


173

В обычном контроллере MVC мы можем вывести pdf с помощью FileContentResult.

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

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

Но как мы можем изменить это в ApiController?

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

Вот что я пробовал, но, похоже, это не работает.

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

Возвращенный результат отображается в браузере:

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

И есть аналогичный пост на SO: Возвращение двоичного файла из контроллера в ASP.NET Web API . Это говорит о выводе существующего файла. Но я не мог заставить его работать с потоком.

Какие-либо предложения?


1
Этот пост помог мне: stackoverflow.com/a/23768883/585552
— Грег

Ответы:


199

Вместо того, чтобы вернуться StreamContentкак Content, я могу заставить его работать 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;
}

2
Если верхняя половина отвечает на ваш вопрос, пожалуйста, оставьте это как ответ. Вторая половина, кажется, другой вопрос - отправьте новый вопрос для этого.
— gunr2171

3
Привет, спасибо, что поделился, получил простой вопрос (наверное). У меня есть интерфейс C #, который получает httpresponsemessage. Как извлечь содержимое потока и сделать его доступным, чтобы пользователь мог сохранить его на диск или что-то еще (и я мог получить реальный файл)? Спасибо!
— Рональд

7
Я пытался скачать самосгенерированный файл Excel. Использование stream.GetBuffer () всегда возвращает поврежденный Excel. Если вместо этого я использую stream.ToArray (), файл генерируется без проблем. Надеюсь, это кому-нибудь поможет.
— afnpires

4
@AlexandrePires Это потому, что MemoryStream.GetBuffer()фактически возвращает буфер MemoryStream, который обычно больше, чем содержимое потока (чтобы сделать вставки эффективными). MemoryStream.ToArray()возвращает урезанный буфер до размера контента.
— М.Страмм

19
Пожалуйста, прекратите делать это. Такого рода злоупотребление MemoryStream вызывает немасштабируемый код и полностью игнорирует назначение потоков. Подумайте: почему byte[]вместо этого все не отображается в виде буферов? Ваши пользователи могут легко запустить ваше приложение из памяти.
— Махдуми

97

Если вы хотите вернуться, IHttpActionResultвы можете сделать это так:

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

3
Хорошее обновление, чтобы показать тип возвращаемого значения IHttpActionResult. Рефакторинг этого кода будет состоять в том, чтобы переместить вызов пользовательского IHttpActionResult, например, указанного в: stackoverflow.com/questions/23768596/…
— Джош,

Этот пост демонстрирует хорошую аккуратную реализацию одноразового использования. В моем случае, вспомогательный метод, указанный в приведенной выше ссылке, оказался более полезным
— hanzolo

45

Этот вопрос помог мне.

Итак, попробуйте это:

Код контроллера:

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

Просмотреть HTML-разметку (с событием клика и простым 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>

1
Здесь вы используете FileStreamдля существующего файла на сервере. Это немного отличается от MemoryStream. Но спасибо за вклад.
— Блез

4
Если вы действительно читаете из файла на веб-сервере, обязательно используйте перегрузку для FileShare.Read, в противном случае вы можете столкнуться с исключениями использования файла.
— Джереми Белл

если заменить его потоком памяти, он не будет работать?
— Алеха

@JeremyBell это просто упрощенный пример, здесь никто не говорит о производственной и отказоустойчивой версии.
— Алеха

1
@Blaise См. Ниже, почему этот код работает, FileStreamно не работает с MemoryStream. Это в основном связано с потоком Position.
— М.Страмм

9

Вот реализация, которая выводит содержимое файла без буферизации (буферизация в byte [] / MemoryStream и т. Д. Может быть проблемой сервера, если это большой файл).

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

Его можно просто использовать так:

public class MyController : ApiController
{
    public IHttpActionResult Get()
    {
        string filePath = GetSomeValidFilePath();
        return new FileResult(filePath);
    }
}

Как бы вы удалили файл после завершения загрузки? Есть ли какие-нибудь хуки, которые нужно уведомить, когда загрузка закончится?
— Коста

Хорошо, похоже, что ответ заключается в реализации атрибута фильтра действий и удалении файла в методе OnActionExecuted.
— Коста

5
Нашел этот пост Ответ Рисорда: stackoverflow.com/questions/2041717/… . Можно использовать эту строку var fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.None, 4096, FileOptions.DeleteOnClose);вместоFile.OpenRead(FilePath)
— Коста

7

Я не совсем уверен, какая часть виновата, но вот почему у MemoryStreamвас не работает:

Когда вы пишете MemoryStream, оно увеличивает свое Positionсвойство. Конструктор StreamContentучитывает текущий поток Position. Поэтому, если вы пишете в поток, а затем передаете его StreamContent, ответ начнется с небытия в конце потока.

Есть два способа исправить это:

1) создавать контент, записывать в поток

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

2) запись в поток, сброс положения, создание содержимого

[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) выглядит немного лучше, если у вас есть свежий поток, 1) проще, если ваш поток не начинается с 0


Этот код на самом деле не дает никакого решения проблемы, так как он использует тот же подход, который был упомянут в вопросе. Вопрос уже гласит, что это не работает, и я могу это подтвердить. return Ok (новый StreamContent (поток)) возвращает JSON-представление StreamContent.
— Дмитрий Захаров

Обновил код. Этот ответ фактически отвечает на более тонкий вопрос «почему простое решение работает с FileStream, а не с MemoryStream», а не как вернуть файл в WebApi.
— М.Страмм

3

Для меня это была разница между

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

и

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

Первый возвращает JSON-представление StringContent: {"Headers": [{"Key": "Content-Type", "Value": ["application / octet-stream; charset = utf-8"]}]}

В то время как второй возвращал файл правильно.

Кажется, что Request.CreateResponse имеет перегрузку, которая принимает строку в качестве второго параметра, и это, кажется, было то, что заставляло сам объект StringContent отображаться в виде строки, а не фактического содержимого.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.