'ASP.NET return dynamic generated binary file without storing entire content in memory

There are a lot of places on the internet that show how to return files but I have found none that will return dynamically generated binary data without storing the whole contents on memory. Maybe I should serialize my data using Json instead of protobufers.

Thanks to this question I was able to create something like this:

[HttpGet]
public ActionResult DownloadItems()
{
    // get 100K items from database as IEnumerable.
    IEnumerable<SomeObject> items = myDatabase.query("my query that returns 100K objects");

    // create memory stream where to place serialized items
    MemoryStream ms = new ();

    // write all serialized items to stream
    foreach(var item in items)
    {
         byte[] itemSerialized = item.BinarySerialize();
         ms.Write(itemSerialized,0,itemSerialized.Length);
    }

    // set position to the begining of memory stream
    ms.Position = 0;

    return File(ms, "application /octet-stream", "foo.bin");
}

This works well but I am loading 100K items into memory. My question is how can I return the same dynamically generated file without having to load all the items into memory?

I remember that the HTTP protocol returns something like this when returning binary files:


HTTP response headers
...

---------SomeGUID--------------

.. binary data

---------SomeGUID--------------

as a result I believe that having something like this will make it work (it has pseudo code):

[HttpGet]
public ActionResult DownloadItems()
{
    // get 100K items from database as IEnumerable.
    IEnumerable<SomeObject> items = myDatabase.query("my query that returns 100K objects");

    // write the begining of file (PSEUDO code)
    this.response.body.writeString("-----------------SomeGuid------------");

    // write all serialized items to stream
    foreach(var item in items)
    {
         byte[] itemSerialized = item.BinarySerialize();
         this.response.body.write(itemSerialized,0,itemSerialized.Length);
    }

    // set position to the begining of memory stream
    ms.Position = 0;

    this.response.body.writeString("-----------------SomeGuid------------");
}

I can install fiddler or any other proxy to see how the real binary transfer of a file looks like. But is there a build in way of doing that so I don't have to go through all that trouble?



Solution 1:[1]

Rather than trying to reuse File() / FileStreamResult, I would recommend implementing your own ActionResult and rendering the content to the response stream there.

public class ByteStreamResult : ActionResult
{
    private readonly IEnumerable<byte[]> blobs;

    public ByteStreamResult(IEnumerable<byte[]> blobs)
    {
        this.blobs = blobs;
    }

    public override async Task ExecuteResultAsync(ActionContext context)
    {
        context.HttpContext.Response.ContentType = "application/octet-stream";
        foreach (var item in blobs)
            await context.HttpContext.Response.Body.WriteAsync(item, context.HttpContext.RequestAborted);
    }
}

return new ByteStreamResult(items.Select(i => i.BinarySerialize()));

Or you could go one step further and implement a custom formatter.

Solution 2:[2]

I just created my own fake file stream for this to work:

public class FakeFileStream : Stream
{
    private readonly IEnumerator<object> _enumerator;
    private bool _completed;

    public FakeFileStream(IEnumerable<object> items)
    {
        if (items is null)
            throw new ArgumentNullException();

        _enumerator = items.GetEnumerator();
    }


    public override int Read(byte[] buffer, int offset, int count)
    {
        if (_enumerator.MoveNext())
        {
            var currentItem = _enumerator.Current;

            // deserialize item.  
            byte[] itemSerialized = currentItem.SerializeUsingDotNetProtoBuf();

            // this will probably not happen but it is a good idea to have it implemented.
            // if this is the case store data on memory and return it on the next read
            if (itemSerialized.Length > buffer.Length)
                throw new NotImplementedException();

            // copy data to buffer
            Buffer.BlockCopy(itemSerialized, 0, buffer, 0, itemSerialized.Length);
            return itemSerialized.Length;
        }
        else
        {
            _completed = true;
            return 0;
        }
    }

    // unused methods
    public override void Flush() => throw new Exception("Unused method");
    public override long Seek(long offset, SeekOrigin origin) => throw new Exception("Unused method");
    public override void SetLength(long value) => throw new Exception("Unused method");
    public override void Write(byte[] buffer, int offset, int count) => throw new Exception("Unused method");

    // Properties
    public override bool CanRead => !_completed;
    public override bool CanSeek => false;
    public override bool CanWrite => false;
    public override long Length => throw new NotImplementedException("Not needed");
    public override long Position
    {
        get => throw new Exception("Unused property");
        set => throw new Exception("Unused property");
    }

    // Implement IDisposable
    public override ValueTask DisposeAsync()
    {
        _enumerator.Dispose();
        return base.DisposeAsync();
    }
}

and My endpoint looks like this:

[HttpGet]
[ProducesResponseType(StatusCodes.Status200OK, Type = typeof(FileStreamResult))]
public IActionResult GetBinary()
{         
    // get some IEnumerable collection 
    IEnumerable<Foo> items = MyDatabase.MyTable.Find("my query");

    // create a fake file stream
    var fs = new FakeFileStream(items);

    return File(fs, "application/octet-stream"); 
}

Edit

Do not use sugested FakeFileStream. For some reason it gave me problems. Maybe because it was writting to the stream very few bytes.

Anywyas I was not able to do this from a controller. But I was able to do it using middleware. I had to do simething like this:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseHttpsRedirection();

// any previous middleware you may have

app.Use(async (context, next) =>
{
    var downloadFile = context.Request.Query["downloadFile"];
    if (!string.IsNullOrWhiteSpace(downloadFile))
    {
        context.Response.ContentType = "application/octet-stream";

        // get 100K items from database as IEnumerable.
        IEnumerable<SomeObject> items = myDatabase.query("my query that returns 100K objects");

    

        // write all serialized items to stream
        foreach(var item in items)
        {
             byte[] itemSerialized = item.BinarySerialize();
             await context.WriteAsync(itemSerialized, context.CancelationToken);
        }

        return;

  
    }


    // Call the next delegate/middleware in the pipeline.
    await next(context);
});

// etc rest of your middleware

app.Run();

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2