Is this possible to stream a FileStreamResult with c# azure function ?

Julien Ferreira 40 Reputation points
2023-06-27T15:12:00.6433333+00:00

Hello,

I'm currently implementing a c# function that will allow me to download several blobs archived in the function itself. To do this, archiving is done in a stream.

Ideally, the function would start to return the zip at the same time as the archiving is taking place. Downloading would then begin directly in the browser.

But I can't get the download to start from the beginning. My current function first archives all the blobs in ram and then returns the zip.

Is this even possible? If so, do you have any ideas?

    <TargetFramework>net6.0</TargetFramework>
    <AzureFunctionsVersion>v4</AzureFunctionsVersion>
class StreamBlobs
    {

        private readonly BlobServiceClient _blobServiceClient;
        private readonly BlobDetails _blobDetails;
        private readonly ILogger _logger;

        public StreamBlobs(BlobServiceClient blobServiceClient, BlobDetails blobDetails, ILogger logger)
        {
            _blobServiceClient = blobServiceClient;
            _blobDetails = blobDetails;
            _logger = logger;
        }
        public Stream ReadBlobAsStream(BlobDetails _blobDetails, ILogger _logger)
        {
            try
            {
                var blobName = Path.GetFileNameWithoutExtension(_blobDetails.InputBlobName) +"."+ _blobDetails.InputBlobFileExtension;
                BlobContainerClient containerClient = _blobServiceClient.GetBlobContainerClient(_blobDetails.SourceContainer);
                _logger.LogWarning(blobName);
                BlobClient blobClient = containerClient.GetBlobClient(blobName);
                Stream stream = new MemoryStream();
                blobClient.DownloadTo(stream);
                return stream;
            }
            catch (Exception ex)
            {
                _logger.LogError("Error in ReadBlobAsStream() " + ex.Message);
                throw;
            }
        }

    }
public class DownloadCollection
    {
        private readonly string _connectionString;
        private BlobServiceClient _storageService;

        public DownloadCollection()
        {
            _connectionString = Environment.GetEnvironmentVariable("STORAGE_ACCOUNT_CONNECTION_STRING");
        }

        [FunctionName("DownloadCollection")]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "download-collection/{id:alpha}")] HttpRequest req, string? id,
            ILogger log)
        {
            log.LogInformation("Download Collection C# HTTP trigger function processed a request.");

            try
            {   
                _storageService = StorageService.Account(_connectionString);
                BlobDetails blobDetails = JsonSerializer.Deserialize<BlobDetails>(@"{ ""InputBlobName"": ""wetweak-welcome-pack"", ""InputBlobFileExtension"": ""zip"", ""SourceContainer"":""welcome-pack""}");
                BlobDetails[] blobs = new BlobDetails[] {
                    JsonSerializer.Deserialize<BlobDetails>(@"{ ""InputBlobName"": ""0f8085be-f3dd-4c8e-8e08-5e616fcbe815"", ""InputBlobFileExtension"": ""mp3"", ""SourceContainer"":""staging-sounds""}"),
                    JsonSerializer.Deserialize<BlobDetails>(@"{ ""InputBlobName"": ""1a7d3c94-0c88-4f4c-beee-14e344232f50"", ""InputBlobFileExtension"": ""fxp"", ""SourceContainer"":""staging-sounds""}"),
                    JsonSerializer.Deserialize<BlobDetails>(@"{ ""InputBlobName"": ""1f72e057-1ac1-47aa-b0d9-8186af0c2fe0"", ""InputBlobFileExtension"": ""fxp"", ""SourceContainer"":""staging-sounds""}"),
                    JsonSerializer.Deserialize<BlobDetails>(@"{ ""InputBlobName"": ""40e518e9-7c0a-413b-92e9-c91e20b20643"", ""InputBlobFileExtension"": ""fxp"", ""SourceContainer"":""staging-sounds""}"),
                    JsonSerializer.Deserialize<BlobDetails>(@"{ ""InputBlobName"": ""565908b1-f4f3-4969-aa50-bdffc08e228c"", ""InputBlobFileExtension"": ""fxp"", ""SourceContainer"":""staging-sounds""}")
                };
                
                Stream res = await GetZipArchive(blobs, _storageService, log);

                return new FileStreamResult(res, "application/octet-stream")
                {
                    FileDownloadName = "collection.zip",
                };
            }
            catch (Exception ex)
            {
                log.LogInformation("Error into ArchiveToZip() " + ex.Message);
                throw;
            }
        }

        public async static Task<Stream> GetZipArchive(BlobDetails[] blobs, BlobServiceClient _storageService, ILogger _logger)
        {
            var archiveStream = new MemoryStream();
            using (var archive = new ZipArchive(archiveStream, ZipArchiveMode.Create, true))
            {
                foreach (var blobDetails in blobs)
                {
                    var memoryStreamBlob = new StreamBlobs(_storageService, blobDetails, _logger);
                    //we get the blob that we need and we add it to a stream
                    var blobAsStream = memoryStreamBlob.ReadBlobAsStream(blobDetails, _logger);
                    var zipArchiveEntry = archive.CreateEntry(blobDetails.InputBlobName, CompressionLevel.Fastest);
                    using var zipStream = zipArchiveEntry.Open();
                    await blobAsStream.CopyToAsync(zipStream);
                }
            }
            archiveStream.Position = 0;
            return archiveStream;
        }
    }

Thanks in advance, regards.

Azure Functions
Azure Functions
An Azure service that provides an event-driven serverless compute platform.
5,399 questions
Azure Blob Storage
Azure Blob Storage
An Azure service that stores unstructured data in the cloud as blobs.
3,068 questions
{count} votes

Accepted answer
  1. navba-MSFT 27,465 Reputation points Microsoft Employee
    2023-07-03T03:53:37.2666667+00:00

    @Julien Ferreira Apologies for the late reply. Welcome to Microsoft Q&A Forum, Thank you for posting your query here!

    Could you please try updating the DownloadCollection class to stream the zip archive as it's being created, instead of first archiving all the blobs in memory and then returning the zip:

    The key changes I have done are below:

    • Instead of returning a FileStreamResult, we write directly to the response stream using response.BodyWriter.AsStream().
    • We pass the response stream to the GetZipArchive method, which creates the ZipArchive directly on the response stream and writes each blob to it as it's being read.
    public class DownloadCollection
    {
        private readonly string _connectionString;
        private BlobServiceClient _storageService;
    
        public DownloadCollection()
        {
            _connectionString = Environment.GetEnvironmentVariable("STORAGE_ACCOUNT_CONNECTION_STRING");
        }
    
        [FunctionName("DownloadCollection")]
        public async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "download-collection/{id:alpha}")] HttpRequest req, string? id,
            ILogger log)
        {
            log.LogInformation("Download Collection C# HTTP trigger function processed a request.");
    
            try
            {   
                _storageService = StorageService.Account(_connectionString);
                BlobDetails blobDetails = JsonSerializer.Deserialize<BlobDetails>(@"{ ""InputBlobName"": ""wetweak-welcome-pack"", ""InputBlobFileExtension"": ""zip"", ""SourceContainer"":""welcome-pack""}");
                BlobDetails[] blobs = new BlobDetails[] {
                    JsonSerializer.Deserialize<BlobDetails>(@"{ ""InputBlobName"": ""0f8085be-f3dd-4c8e-8e08-5e616fcbe815"", ""InputBlobFileExtension"": ""mp3"", ""SourceContainer"":""staging-sounds""}"),
                    JsonSerializer.Deserialize<BlobDetails>(@"{ ""InputBlobName"": ""1a7d3c94-0c88-4f4c-beee-14e344232f50"", ""InputBlobFileExtension"": ""fxp"", ""SourceContainer"":""staging-sounds""}"),
                    JsonSerializer.Deserialize<BlobDetails>(@"{ ""InputBlobName"": ""1f72e057-1ac1-47aa-b0d9-8186af0c2fe0"", ""InputBlobFileExtension"": ""fxp"", ""SourceContainer"":""staging-sounds""}"),
                    JsonSerializer.Deserialize<BlobDetails>(@"{ ""InputBlobName"": ""40e518e9-7c0a-413b-92e9-c91e20b20643"", ""InputBlobFileExtension"": ""fxp"", ""SourceContainer"":""staging-sounds""}"),
                    JsonSerializer.Deserialize<BlobDetails>(@"{ ""InputBlobName"": ""565908b1-f4f3-4969-aa50-bdffc08e228c"", ""InputBlobFileExtension"": ""fxp"", ""SourceContainer"":""staging-sounds""}")
                };
                
                var response = req.HttpContext.Response;
                response.ContentType = "application/octet-stream";
                response.Headers.Add("Content-Disposition", "attachment; filename=\"collection.zip\"");
    
                await GetZipArchive(blobs, _storageService, log, response.BodyWriter.AsStream());
    
                return new OkResult();
            }
            catch (Exception ex)
            {
                log.LogInformation("Error into ArchiveToZip() " + ex.Message);
                throw;
            }
        }
    
        public async static Task GetZipArchive(BlobDetails[] blobs, BlobServiceClient _storageService, ILogger _logger, Stream outputStream)
        {
            using (var archive = new ZipArchive(outputStream, ZipArchiveMode.Create, true))
            {
                foreach (var blobDetails in blobs)
                {
                    var memoryStreamBlob = new StreamBlobs(_storageService, blobDetails, _logger);
                    //we get the blob that we need and we add it to a stream
                    var blobAsStream = memoryStreamBlob.ReadBlobAsStream(blobDetails, _logger);
                    var zipArchiveEntry = archive.CreateEntry(blobDetails.InputBlobName, CompressionLevel.Fastest);
                    using var zipStream = zipArchiveEntry.Open();
                    await blobAsStream.CopyToAsync(zipStream);
                }
            }
        }
    }
    

    Hope this helps.

    ** Please do not forget to "Accept the answer” and “up-vote” wherever the information provided helps you, this can be beneficial to other community members.


1 additional answer

Sort by: Most helpful
  1. Julien Ferreira 40 Reputation points
    2023-07-19T11:59:27.4833333+00:00

    Thank you for your reply. I tried the proposed solution as is but the compiler reports that there is no BodyWriter attribute in the HttpResponse class. I can change it to Body which is the Stream type attribute of the class and the compiler agrees with everything.

    However, when I start the function pool and try to call my function, I get the following error:

    User's image

    Do you have any idea where this might be coming from? Didn't you have the same error ?

    Thanks in advance, best regards.


Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.