Streaming XmlWriter to the Http response while being read by user

179 Views Asked by At

I am trying to generate an XML file from a query result by using the following method which I then return it via a ASP.net core web API:

public static async Task ExportFile(IDbConnection _dbConnection, Stream memoryStream, CancellationToken cancellationToken = default){

            using (XmlWriter writer = XmlWriter.Create(memoryStream, settings))
            {
                writer.WriteStartDocument();
                using (SqlConnection connection = new SqlConnection(_dbConnection.ConnectionString))
                {
                    SqlCommand cmd = connection.CreateCommand();

                    //Some irrevelant codes

                        writer.WriteStartElement("table");
                        using var reader = await cmd.ExecuteReaderAsync(cancellationToken);

                        while (reader.Read())
                        {
                            writer.WriteStartElement("record");

                            for (int i = 0; i < reader.VisibleFieldCount; i++)
                            {
                                writer.WriteElementString( reader.GetName(i), reader.GetValue(i).ToString());
                            }

                            writer.WriteEndElement();
                        }

                       writer.WriteEndElement();

}

As it might be inefficient to store the whole file in the memory until user downloads it completely, how can I make the thread sleep until the user download a buffered result? My point to use XmlWriter was to avoid storing the whole file in the memory.

I tried to achieve this with I/O pipelines but couldn't find any relevant example. Also tried to use var stream = Response.Body; as the stream but it didn't return anything in the response.

public async Task<IActionResult> ExportFile([Required] string uid, string name, CancellationToken cancellationToken=default)
        {
                var syncIOFeature = HttpContext.Features.Get<IHttpBodyControlFeature>();
                if (syncIOFeature != null)
                {
                    syncIOFeature.AllowSynchronousIO = true;
                }

        await ExportFile(_dbConnection, Response.Body, new Metadata(name,uid), cancellationToken);
        return Ok(Response);
}

Search results were mostly about filestream or httpclient which can't be applied to problem.

I think I need to buffer the result and sleep the ExportFile thread until the buffer is consumed, but I don't know how to achieve this with XmlWriter.

1

There are 1 best solutions below

2
Jason Pan On

I am reading the Pipe basic usage. And write the sample code below for you.

    [HttpGet("export")]
    public async Task ExportFile()
    {
        var pipe = new Pipe();

        var write_Task = WriteToPipeAsync(pipe.Writer);
        var read_Task = ReadFromPipeAndWriteToResponseAsync(pipe.Reader, Response.Body);
        // Set the content type for XML
        //Response.ContentType = "application/xml; charset=utf-8";

        // Indicate that it's a file attachment and provide a default file name
        //Response.Headers["Content-Disposition"] = "attachment; filename=export.xml";

        await Task.WhenAll(write_Task, read_Task);
    }

    private async Task WriteToPipeAsync(PipeWriter writer)
    {
        var bufferSize = 4096;

        using (var connection = new SqlConnection(@"Server=(localdb)\MSSQLLocalDB;Database=mylDb;Trusted_Connection=True;MultipleActiveResultSets=true"))
        {
            try
            {
                await connection.OpenAsync();
            }
            catch (Exception ex)
            {

                throw ex;
            }
           
            using var cmd = connection.CreateCommand();
            // Your SQL query here
            cmd.CommandText = "SELECT * FROM Table2";

            using var reader = await cmd.ExecuteReaderAsync();

            while (await reader.ReadAsync())
            {
                var memory = writer.GetMemory(bufferSize);
                using (var stream = new MemoryStream(memory.ToArray()))
                using (var xmlWriter = XmlWriter.Create(stream, new XmlWriterSettings
                {
                    Encoding = Encoding.UTF8,
                    Async = true
                }))
                {
                    xmlWriter.WriteStartElement("record");
                    for (int i = 0; i < reader.FieldCount; i++)
                    {
                        xmlWriter.WriteElementString(reader.GetName(i), reader.GetValue(i).ToString());
                    }
                    // record
                    try
                    {
                        xmlWriter.WriteEndElement();
                        await xmlWriter.FlushAsync();
                    }
                    catch (Exception ex)
                    {

                        throw ex;
                    }
                   
                }
                // Replace this with actual bytes written
                var bytesWritten = (int)memory.Length;
                writer.Advance(bytesWritten);

                // Signal the reader that there's data to read
                var flushResult = await writer.FlushAsync();

                // Check if the reader is completed
                if (flushResult.IsCompleted)
                {
                    break;
                }
            }

            writer.Complete();
        }
    }

    private async Task ReadFromPipeAndWriteToResponseAsync(PipeReader reader, Stream responseBody)
    {
        while (true)
        {
            var readResult = await reader.ReadAsync();
            var buffer = readResult.Buffer;

            foreach (var segment in buffer)
            {
                await responseBody.WriteAsync(segment);
            }

            reader.AdvanceTo(buffer.End);

            if (readResult.IsCompleted)
            {
                break;
            }
        }

        reader.Complete();
    }