Как вернуть файл (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;
}
Ogglas
источник
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>
Aleha
источник
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

M.Stramm
источник
Этот код на самом деле не дает никакого решения проблемы, так как он использует тот же подход, который был упомянут в вопросе. Вопрос уже гласит, что это не работает, и я могу это подтвердить. 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 отображаться в виде строки, а не фактического содержимого.

Эндер Виггин
источник