8

Using the MVC model, I would like to write a JsonResult that would stream the Json string to the client rather than converting all the data into Json string at once and then streaming it back to the client. I have actions that require to send very large (over 300,000 records) as Json transfers and I think the basic JsonResult implementation is not scalable.

I am using Json.net, I am wondering if there is a way to stream the chunks of the Json string as it is being transformed.

//Current implementation:
response.Write(Newtonsoft.Json.JsonConvert.SerializeObject(Data, formatting));
response.End();

//I know I can use the JsonSerializer instead
Newtonsoft.Json.JsonSerializer serializer = new Newtonsoft.Json.JsonSerializer();
serializer.Serialize(textWriter, Data);

However I am not sure how I can get the chunks written into textWriter and write into response and call reponse.Flush() until all 300,000 records are converted to Json.

Is this possible at all?

dbc
  • 104,963
  • 20
  • 228
  • 340
sam360
  • 1,121
  • 3
  • 18
  • 37

2 Answers2

18

Assuming your final output is a JSON array and each "chunk" is one item in that array, you could try something like the following JsonStreamingResult class. It uses a JsonTextWriter to write the JSON to the output stream, and uses a JObject as a means to serialize each item individually before writing it to the writer. You could pass the JsonStreamingResult an IEnumerable implementation which can read items individually from your data source so that you don't have them all in memory at once. I haven't tested this extensively, but it should get you going in the right direction.

public class JsonStreamingResult : ActionResult
{
    private IEnumerable itemsToSerialize;

    public JsonStreamingResult(IEnumerable itemsToSerialize)
    {
        this.itemsToSerialize = itemsToSerialize;
    }

    public override void ExecuteResult(ControllerContext context)
    {
        var response = context.HttpContext.Response;
        response.ContentType = "application/json";
        response.ContentEncoding = Encoding.UTF8;

        JsonSerializer serializer = new JsonSerializer();

        using (StreamWriter sw = new StreamWriter(response.OutputStream))
        using (JsonTextWriter writer = new JsonTextWriter(sw))
        {
            writer.WriteStartArray();
            foreach (object item in itemsToSerialize)
            {
                JObject obj = JObject.FromObject(item, serializer);
                obj.WriteTo(writer);
                writer.Flush();
            }
            writer.WriteEndArray();
        }
    }
}
Brian Rogers
  • 125,747
  • 31
  • 299
  • 300
  • The solution worked which prevents out of memory exception and that's wonderful. But i think it would be more optimized if batches of records would be flushed together rather than one by one. Not sure what the optimum number is! – sam360 Oct 11 '14 at 01:09
  • Yeah, I wondered about that too. You could easily add a counter to the JsonStreamingResult which causes it to wait to Flush until some number of records has been read from the enumerable. If the number is different depending on the situation, you could make it a parameter so that you can tune it for each different use. Also, on the IEnumerable side, you could implement a mechanism to query your data source in batches as well to improve efficiency there. You'd have to do lots of measuring and testing to see what works the best though. – Brian Rogers Oct 11 '14 at 01:51
  • Another idea although may not be possible is to measure the buffer size and flush at every 64KB or something like that. Not sure if we can check the size of data in the JsonTextWriter – sam360 Oct 11 '14 at 02:23
  • 2
    If you're looking to do something like that, you could try wrapping the `OutputStream` with a [`BufferedStream`](http://msdn.microsoft.com/en-us/library/system.io.bufferedstream(v=vs.110).aspx). However, [this Q & A](http://stackoverflow.com/questions/492283/when-to-use-net-bufferedstream-class) seems to indicate that most streams in .NET are already pretty well optimized as far as buffering goes. If that is the case, perhaps it is better not to call `Flush` at all, and just let the stream do its thing when its internal buffer gets full. Not sure though; you'd have to test it. – Brian Rogers Oct 11 '14 at 03:58
  • 1
    Some benchmarking showed that the most efficient way is to use serializer.Serialize(writer, data); and pass all the data to it at once, as the above comment puts out the Stream itself does pretty good job of handling the buffer, and you code won't need to do one huge loop :) – sam360 Oct 16 '14 at 02:27
  • @BrianRogers How did you used this in actual application, can you please post some code here? – jkyadav Jun 11 '16 at 06:33
0

The problem with leaving it up to .NET and wait until the buffer is full has other problems.

For example: If you do that some of the contents for the json will cut off causing parsing issues on the frontend.

Best approach so far is to flush the batch on each iteration in the event you do use a batch or flush it per single item if that's what your design is for.

Currently i use SSE to push to the data to browser and a delimiter message 'on message end' to indicate to the broswer that the connection can be closed, i know SSE use case is for continuous stream but we can also use it to help with chunking and batching response.