user3299116
user3299116

Reputation: 43

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.

Upvotes: 1

Views: 641

Answers (1)

David Browne - Microsoft
David Browne - Microsoft

Reputation: 89361

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

Upvotes: 1

Related Questions