238

In a regular MVC controller, we can output pdf with a FileContentResult.

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

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

But how can we change it into an ApiController?

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

Here is what I've tried but it doesn't seem to work.

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

The returned result displayed in the browser is:

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

And there is a similar post on SO: Returning binary file from controller in ASP.NET Web API . It talks about output an existing file. But I could not make it work with a stream.

Any suggestions?

Community
  • 1
  • 1
Blaise
  • 21,314
  • 28
  • 108
  • 169

8 Answers8

250

Instead of returning StreamContent as the Content, I can make it work with 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;
}
huysentruitw
  • 27,376
  • 9
  • 90
  • 133
Blaise
  • 21,314
  • 28
  • 108
  • 169
  • 4
    If the top half answers your question, please only post that as an answer. The second half appears to be a different question - post a new question for that. – gunr2171 Dec 15 '14 at 18:21
  • 5
    Hi, thanks for sharing, got a simple question (I guess). I have a C# front end that receives the httpresponsemessage. How do I extract the streamcontent and make it available so a user can save it to disk or something (and I can get the actual file)? Thanks! – Ronald Aug 24 '15 at 10:06
  • 10
    I was trying to download a self generated excel file. Using the stream.GetBuffer() always returned an corrupted excel. If instead I use stream.ToArray() the file is generated without a problem. Hope this helps someone. – afnpires Apr 05 '16 at 15:07
  • 8
    @AlexandrePires That is because `MemoryStream.GetBuffer()` actually returns the MemoryStream's buffer, which is usually larger than the stream's content (to make insertions efficient). `MemoryStream.ToArray()` returns the buffer truncated to the content size. – M.Stramm Jul 28 '16 at 00:54
  • @AlexandrePires Thank you so much. This was my problem as well – Jonas Olesen Sep 30 '16 at 08:00
  • Because I was returning a large file, I needed to use the stream to buffer the contents rather than read the entire file into memory up-front. I found this answer from a different post helpful: http://stackoverflow.com/a/9549889/540061 – Robb Sadler Jan 26 '17 at 23:02
  • 1
    thanks @AlexandrePires that was my case also. I edited this answer because I don't see practical reason why someone would like to get actual underlying array that can be bigger then actual data saved to MemoryStream. – Mariusz Pawelski Feb 10 '17 at 11:15
  • 42
    Please stop doing this. This sort of abuse of MemoryStream causes, unscalable code and completely ignores the purpose of Streams. Think: why isn't everything just exposed as `byte[]` buffers instead? Your users can easily run your application out of memory. – makhdumi Apr 06 '17 at 23:49
  • 2
    @makhdumi What's the alternative to MemoryStream? – Ian Warburton Oct 19 '18 at 20:50
  • 2
    @IanWarburton Streams should be streamed into one another using small, typically ~4 KB, buffers. .NET conveniently provides a method for this, [Stream.CopyTo](https://learn.microsoft.com/en-us/dotnet/api/system.io.stream.copyto?view=netframework-4.7.2) – makhdumi Oct 22 '18 at 14:00
  • 1
    @makhdumi Unless a webpage is using chunking when rendering, all the data will be in memory anyhow. – Ian Warburton Oct 23 '18 at 14:22
  • This doesn't work for me. It does appear to return the PDR as a byte array to the front end, but then when i try to open it, i get this error "Failed to load PDF document". – lukegf Mar 07 '19 at 16:30
  • You should be able to use `stream.GetBuffer()` as long as you specify the actual size using the [`ByteArrayContent(Byte[], Int32, Int32)`](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.bytearraycontent.-ctor?view=netcore-3.1#System_Net_Http_ByteArrayContent__ctor_System_Byte___System_Int32_System_Int32_) constructor: `new ByteArrayContent(stream.GetBuffer(), 0, checked((int)stream.Length))`. This will be much more memory-efficient as `ToArray()` actually copies the contents of the internal buffer. – dbc Aug 29 '20 at 18:53
  • for file stored in a path, [this](https://stackoverflow.com/a/58188829/9746445) worked for me. – Tulshi Das Jan 11 '21 at 07:01
  • Answer is incomplete. Where is the File to download. How to extract the file from HttpResponseMessage for users to download. – Sujoy Feb 19 '23 at 08:17
116

If you want to return IHttpActionResult you can do it like this:

[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;
}
Nkosi
  • 235,767
  • 35
  • 427
  • 472
Ogglas
  • 62,132
  • 37
  • 328
  • 418
  • 3
    Good update to show IHttpActionResult return type. A refactor of this code would be to move call a custom IHttpActionResult such as the one listed at: http://stackoverflow.com/questions/23768596/why-is-my-file-not-being-returned-by-a-get-request-from-my-web-api-function – Josh May 26 '16 at 21:19
  • This post demonstrates a nice tidy single use implementation. In my case, the helper method listed in the above link proved more helpful – hanzolo Mar 29 '17 at 22:59
  • 1
    If you vote down please say why, hard to improve answers otherwise. – Ogglas Aug 26 '20 at 13:53
  • I think you can only use `stream.GetBuffer()` if you specify the actual size using the [`ByteArrayContent(Byte[], Int32, Int32)`](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.bytearraycontent.-ctor?view=netcore-3.1#System_Net_Http_ByteArrayContent__ctor_System_Byte___System_Int32_System_Int32_) constructor: `new ByteArrayContent(stream.GetBuffer(), 0, checked((int)stream.Length))`. The internal buffer array may be longer than the actual contents so specifying the content length is necessary. – dbc Aug 29 '20 at 19:14
53

This question helped me.

So, try this:

Controller code:

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

View Html markup (with click event and simple 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_84
  • 8,309
  • 2
  • 38
  • 46
  • 1
    Here you are using `FileStream` for an existing file on the server. It is a bit different from `MemoryStream`. But thanks for the input. – Blaise Sep 25 '14 at 14:26
  • 5
    If you do read from a file on a web server, be sure to use the overload for FileShare.Read, otherwise you may encounter file in use exceptions. – Jeremy Bell Sep 25 '14 at 14:34
  • if you replace it with memory stream it will not work? – aleha_84 Sep 25 '14 at 14:34
  • @JeremyBell it is just a simpified example, nobody talks here about production and fail-safe version. – aleha_84 Sep 25 '14 at 14:35
  • @aleha, no it won't work with `MemoryStream`. It feels strange. I returned `ByteArrayContent` in the end. (see my answer below) Hope some expert in SO can explain the difference. – Blaise Sep 25 '14 at 14:40
  • 1
    @Blaise See below for why this code works with `FileStream` but fails with `MemoryStream`. It's basically got to do with the Stream's `Position`. – M.Stramm Jul 28 '16 at 00:49
25

Here is an implementation that streams the file's content out without buffering it (buffering in byte[] / MemoryStream, etc. can be a server problem if it's a big file).

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

It can be simply used like this:

public class MyController : ApiController
{
    public IHttpActionResult Get()
    {
        string filePath = GetSomeValidFilePath();
        return new FileResult(filePath);
    }
}
Simon Mourier
  • 132,049
  • 21
  • 248
  • 298
  • 1
    How would you delete the file after the download is done? Are there any hooks to be notified when the download is finished? – boggy Nov 07 '18 at 20:15
  • 1
    ok, the answer seems to be to implement an action filter attribute and remove the file in the OnActionExecuted method. – boggy Nov 07 '18 at 20:45
  • 7
    Found this post Risord's answer: https://stackoverflow.com/questions/2041717/how-to-delete-file-after-download-with-asp-net-mvc. One can use this line `var fs = new FileStream(FilePath, FileMode.Open, FileAccess.Read, FileShare.None, 4096, FileOptions.DeleteOnClose);` instead of `File.OpenRead(FilePath)` – boggy Nov 07 '18 at 22:21
8

I am not exactly sure which part to blame, but here's why MemoryStream doesn't work for you:

As you write to MemoryStream, it increments its Position property. The constructor of StreamContent takes into account the stream's current Position. So if you write to the stream, then pass it to StreamContent, the response will start from the nothingness at the end of the stream.

There's two ways to properly fix this:

  1. construct content, write to stream

     [HttpGet]
     public HttpResponseMessage Test()
     {
         var stream = new MemoryStream();
         var response = Request.CreateResponse(HttpStatusCode.OK);
         response.Content = new StreamContent(stream);
         // ...
         // stream.Write(...);
         // ...
         return response;
     }
    
  2. write to stream, reset position, construct content

     [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;
     }
    
  3. looks a little better if you have a fresh Stream, 1) is simpler if your stream does not start at 0

Nigel Thorne
  • 21,158
  • 3
  • 35
  • 51
M.Stramm
  • 1,289
  • 15
  • 29
  • 1
    This code actually does not provide any solution to the problem, as it uses the same approach that was mentioned int the question. The question already states that this does not work, and I can confirm that. return Ok(new StreamContent(stream)) returns JSON representation of StreamContent. – Dmytro Zakharov Jul 27 '16 at 19:47
  • Updated the code. This answer actually answers the more subtle question of 'why does the simple solution work with FileStream but not MemoryStream' rather than how to return a File in WebApi. – M.Stramm Jul 28 '16 at 01:05
6

For me it was the difference between

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

and

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

The first one was returning the JSON representation of StringContent: {"Headers":[{"Key":"Content-Type","Value":["application/octet-stream; charset=utf-8"]}]}

While the second one was returning the file proper.

It seems that Request.CreateResponse has an overload that takes a string as the second parameter and this seems to have been what was causing the StringContent object itself to be rendered as a string, instead of the actual content.

EnderWiggin
  • 525
  • 6
  • 7
2

I found this article useful: https://codeburst.io/download-files-using-web-api-ae1d1025f0a9

Basically it says:

[Route("api/[controller]")]
[ApiController]
public class JobController : ControllerBase
{
    [HttpGet]
    public ActionResult GetFile()
    {
        byte[] fileContent = GetFile();
        return File(fileContent, "application/pdf", "test.pdf");
    }
}
Jon
  • 1,060
  • 8
  • 15
-3
 var memoryStream = new MemoryStream();
                await cloudFile.DownloadToStreamAsync(memoryStream);
                responseMessage.result = "Success";

                var contentType = "application/octet-stream";
            
                **using (var stream = new MemoryStream())
                {                    
                    return File(memoryStream.GetBuffer(), contentType, "Cartage.pdf");
                }**
  • 4
    Please add further details to expand on your answer, such as working code or documentation citations. – Community Aug 28 '21 at 16:18