I have files stored in Azure Storage in the form of blobs, one blob per file. Files sizes can go up to ~100MB in size.
I am implementing an API that serves these files to consumers as instances of ASP.Net Core 3.1's IFormFile interface.
In my server-side implementation of the API I am downloading the files using Azure's BlobClient.DownloadAsync() method. This returns an instance of BlobDownloadInfo that contains all the data that I need. Specifically, the file's contents are made available through the BlobDownloadInfo.Value.Content property which, at runtime, contains an instance of RetriableStreamImpl
I am mapping from the BlobDownaldInfo into an IFormFile using the default implementation FileInfo class like so:
BlobDownloadInfo response = await blobClient.DownloadAsync();
IFormFile file = new FormFile(
response.Value.Content,
0, // baseStreamOffset
response.Value.ContentLength,
"foo",
"bar.txt")
{
Headers = new HeaderDictionary(),
ContentType = response.Value.ContentType,
ContentDisposition = response.Value.Details.ContentDisposition
};
Unfortunately for me, this code throws :
System.NotSupportedException : Specified method is not supported. at Azure.Core.Pipeline.RetriableStream.RetriableStreamImpl.set_Position(Int64 value)
It looks to me as though the FormFile constructor is trying to set the stream Position to 0 but that that stream is a RetriableStreamImpl which does not allow setting its Position property.
I've worked around this issue by copying the RetriableStreamImpl contents into a MemoryStream and passing that into the FormFile constructor:
var memoryStream = new MemoryStream();
response.Value.Content.CopyTo(memoryStream);
This works and my question is - Is this a good solution? Are there any performance issues I need to address now?
Copying the file to a memory stream is a bad idea. It will slow down the whole process and if many downloads are happening at the same time, you will overload your system's memory.
Instead, you should try to "pass through" the stream from blob storage to the client. We usually do this using FileStreamResult and blob.OpenReadAsync. blobClient.DownloadAsync looks fine for me, so you could try replacing IFile with FileStreamResult.
Some code to get you started:
var blob = container.GetBlockBlobReference(path);
var stream = blob.OpenReadAsync(cancellationToken);
// ...
// Controller action:
return new FileStreamResult(stream, contentType)
{
FileDownloadName = "myfile.txt" // => ContentDisposition Header
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With