ASP.NET return 动态生成的二进制文件,不将整个内容存储在内存中

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

互联网上有很多地方展示了如何 return 文件,但我发现 none 将 return 动态生成二进制数据而不存储全部内容记忆。也许我应该使用 Json 而不是 protobufers 来序列化我的数据。

感谢 this question 我能够创建这样的东西:

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

这很好用,但我正在将 100K 项加载到内存中。我的问题是我如何 return 相同的动态生成文件而不必将所有项目加载到内存中?

我记得当 returning 二进制文件时,HTTP 协议 return 是这样的:


HTTP response headers
...

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

.. binary data

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

因此我相信有这样的东西会让它工作(它有伪代码):

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

我可以安装 fiddler 或任何其他代理来查看文件的真实二进制传输情况。但是有没有一种构建方法可以让我不必经历所有这些麻烦?

我刚刚为此创建了自己的假文件流:

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

我的端点如下所示:

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

编辑

不要使用建议 FakeFileStream。出于某种原因,它给了我一些问题。可能是因为它写入流的字节很少。

无论如何,我无法通过控制器执行此操作。但是我能够使用中间件来做到这一点。我不得不这样做:

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

与其尝试重用 File() / FileStreamResult,我建议实施您自己的 ActionResult 并将内容呈现到那里的响应流。

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

或者您可以更进一步,实施 custom formatter