Search code examples
c#.net.net-coresqlclientdeflatestream

Decompress SQL Blob Content and return the answer in PushStreamContent .NET Core


I am developing a new API in a .NET Core service, the new API is supposed to read a BLOB from SQL table, decompress it using DeflateStream. And then return it (stream it) to the client.

In order not to consume much memory. I am returning a response of type and PushStreamContent so that I could copy the sql stream into the response stream directly without loading the blob in memory. So I ended up with something like that.

return this.ResponseMessage(new HttpResponseMessage
        {
            Content = new PushStreamContent(async (outStream, httpContent, transportContext) =>
            {
                using (SqlConnection connection = new SqlConnection(connectionString))
                {
                    await connection.OpenAsync();
                    using (SqlCommand command = new SqlCommand(query, connection))
                    {

                        // The reader needs to be executed with the SequentialAccess behavior to enable network streaming
                        // Otherwise ReadAsync will buffer the entire BLOB into memory which can cause scalability issues or even OutOfMemoryExceptions
                        using (SqlDataReader reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess))
                        {
                            if (await reader.ReadAsync() && !(await reader.IsDBNullAsync(0)))
                            {
                                using (Stream streamToDecompress = reader.GetStream(0))
                                using (Stream decompressionStream = new DeflateStream(streamToDecompress, CompressionMode.Decompress))
                                {
                                    // This copyToAsync will take for ever
                                    await decompressionStream.CopyToAsync(outStream);
                                    outStream.close();

                                    return;
                                }
                            }

                            throw new Exception("Couldn't retrieve blob");
                        }
                    }
                }
            },
            "application/octet-stream")
        });

The problem here is that the step that copies the deflateStream to the response output stream takes for ever as mentioned in the code. Although I tried the same exact method but with writing the stream to a file instead of copying it to the resp stream and it worked like a charm.

So can you guys help me with this?? Am I wrong about using the PushStreamContent? Should I use a different approach? The thing is that I don't want to load the whole Blob in memory, I want to read it and decompress it on the fly. SqlClient Supports streaming blobs and I want to make use of that.


Solution

  • This is a deadlock in PushStreamContent, which I don't pretend to understand. But I repro'd it and changing

    await decompressionStream.CopyToAsync(outStream);
    

    to

    decompressionStream.CopyTo(outStream);
    

    resolves it.

    Here's the full repro:

    public ResponseMessageResult Get()
    {
        var data =  new string[] { "value1", "value2" };
    
        var jsonData = Newtonsoft.Json.JsonConvert.SerializeObject(data);
    
        var msSource = new MemoryStream(Encoding.UTF8.GetBytes(jsonData));
        var msDest = new MemoryStream();
        var compressionStream = new DeflateStream(msDest, CompressionMode.Compress);
        msSource.CopyTo(compressionStream);
        compressionStream.Close();
    
        var compressedBytes = msDest.ToArray();
    
        var query = "select @bytes buf";
        var connectionString = "server=localhost;database=tempdb;integrated security=true";
    
        
        return this.ResponseMessage(new HttpResponseMessage
        {
            Content = new PushStreamContent(async (outStream, httpContent, transportContext) =>
            {
                using (SqlConnection connection = new SqlConnection(connectionString))
                {
                    await connection.OpenAsync();
                    using (SqlCommand command = new SqlCommand(query, connection))
                    {
                        command.Parameters.Add("@bytes", SqlDbType.VarBinary, -1).Value = compressedBytes;
    
                        // The reader needs to be executed with the SequentialAccess behavior to enable network streaming
                        // Otherwise ReadAsync will buffer the entire BLOB into memory which can cause scalability issues or even OutOfMemoryExceptions
                        using (SqlDataReader reader = await command.ExecuteReaderAsync(CommandBehavior.SequentialAccess))
                        {
                            if (await reader.ReadAsync() && !(await reader.IsDBNullAsync(0)))
                            {
                                using (Stream streamToDecompress = reader.GetStream(0))
                                {
                                    //var buf = new MemoryStream();
                                    //streamToDecompress.CopyTo(buf);
                                    //buf.Position = 0;
    
                                    using (Stream decompressionStream = new DeflateStream(streamToDecompress, CompressionMode.Decompress))
                                    {
                                        
                                        // This copyToAsync will take for ever
                                        //await decompressionStream.CopyToAsync(outStream);
                                        decompressionStream.CopyTo(outStream);
                                        outStream.Close();
    
                                        return;
                                    }
                                }
                            }
    
                            throw new Exception("Couldn't retrieve blob");
                        }
                    }
                }
            },
        "application/octet-stream")
        });
    }