Як повернути файл (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.