Як повернути файл (FileContentResult) в ASP.NET WebAPI


173

У звичайному контролері MVC ми можемо виводити PDF у форматі a 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 #, який приймає httpsponsemessage. Як витягти streamcontent і зробити його доступним, щоб користувач міг зберегти його на диску чи щось (і я можу отримати фактичний файл)? Дякую!
— Рональд

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/…
— Josh

Цей пост демонструє приємну охайну реалізацію одного використання. У моєму випадку хелперний метод, перелічений у вищенаведеному посиланні, виявився більш корисним
— 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, інакше ви можете зіткнутися з файлом у винятках використання.
— Джеремі Белл

якщо замінити його потоком пам'яті, він не працюватиме?
— aleha

@JeremyBell - це просто спрощений приклад, тут ніхто не говорить про виробництво та безпечну версію.
— aleha

1
@Blaise Дивіться нижче, чому цей код працює, FileStreamале не вдається MemoryStream. В основному це стосується потоків Position.
— М.Страмм

9

Ось реалізація, яка виводить вміст файлу без буферизації (буферизація в байті [] / 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
Знайдено відповідь цей повідомленням Risord в: stackoverflow.com/questions/2041717 / ... . Можна використовувати цей рядок var fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.None, 4096, FileOptions.DeleteOnClose);замістьFile.OpenRead(FilePath)
— costa

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.