Overview
When it comes to streaming a large array of objects (for example, 10,000 objects), we are usually required to deal with two major performance issues:
- Large memory allocation for objects
- Long response time from server
To deal with the issues, we have two methods that can improve server side performance:
- Iterative Pattern in C#
- Chunked Transfer Encoding in HTTP
In the following sections, we will take a look at these methods to see how they help two issues out. We will also see two examples working on the array streaming, from server side to client side.
Iterative Pattern in C#
It has been pretty well known that we can enable Iterative Pattern by using yield
keyword within a method or property which has IEnumerable(T)
return type. The idea behind the pattern is to enumerate each of the items instead of returning the whole collection.
public IEnumerable<ReturnModel> Get()
{
foreach (var i in Enumerable.Range(0, 10000))
yield return new ReturnModel() { SequenceNumber = i, ID = Guid.NewGuid() };
}
Because the enumeration starts as soon as the foreach
loop goes without waiting for all objects to be ready, we can expect that the efficiency and memory use are better in general.
Chunked Transfer Encoding in HTTP
Chunked Transfer Encoding is a mechanism that allows the server to return the data "piece by piece". In the encoding, data are separated by each hexadecimal number followed by a "\r\n
" which tells the client the length of the following chunk. Below is a server response example given by Mozilla Developer Network that consists of three lines and each line is a chunk.
HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked
7\r\n
Mozilla\r\n
9\r\n
Developer\r\n
7\r\n
Network\r\n
0\r\n
\r\n
Because the data is aimed to be sent in a series of chunks instead of the whole one, the normal Content-Length
header is omitted.
Server Side Example
The following code snippet shows a Web API method transmitting a large JSON array. The Get
method returns each object in Iterative Pattern which we have taken a look at before. A customized HTTP Message Handler will enable Chunked Transfer Encoding before return stream starts.
namespace WebApplication1.Controllers
{
public class ValuesController : ApiController
{
[HttpGet]
public IEnumerable<ReturnModel> Get()
{
foreach (var i in Enumerable.Range(0, 10000))
yield return new ReturnModel() { SequenceNumber = i, ID = Guid.NewGuid() };
}
}
public class Handler : DelegatingHandler
{
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request,
CancellationToken cancellationToken)
{
var response = base.SendAsync(request, cancellationToken);
response.Result.Headers.TransferEncodingChunked = true;
return response;
}
}
[DataContract]
public class ReturnModel
{
[DataMember]
public int SequenceNumber { get; set; }
[DataMember]
public Guid ID { get; set; }
}
}
The following code snippet shows how to add our customized HTTP message handler to the collection.
namespace WebApplication1
{
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
config.MessageHandlers.Add(new Handler());
}
}
}
Client Side Example
The next code snippet demonstrates a console application consuming the JSON array from server. With JsonTextReader
and JsonSerializer
, the client application can start enumerating each object in the array without waiting for the whole JSON data to be transmitted.
namespace ConsoleApp1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Press any key to continue...");
Console.ReadKey(true);
foreach (var value in GetValues())
Console.WriteLine("{0}\t{1}", value.SequenceNumber, value.ID);
Console.ReadKey(true);
}
static IEnumerable<ReturnModel> GetValues()
{
var serializer = new JsonSerializer();
var client = new HttpClient();
var header = new MediaTypeWithQualityHeaderValue("application/json");
client.DefaultRequestHeaders.Accept.Add(header);
using (var stream = client.GetStreamAsync
("http://localhost:63025/api/values").Result)
using (var sr = new StreamReader(stream))
using (var jr = new JsonTextReader(sr))
{
while (jr.Read())
{
if (jr.TokenType != JsonToken.StartArray && jr.TokenType != JsonToken.EndArray)
yield return serializer.Deserialize<ReturnModel>(jr);
}
}
}
}
}
You probably have noticed that there is nothing that the client application needs to do for Chunked Transfer Encoding. The reason is that by default, Chunked Transfer Encoding is expected to be accepted by clients. Since HttpClient
has already handled it behind-the-scenes, we do not need to take extra action for the encoding.
References
History
- 2017-05-05 Initial post
- 2017-05-05 Added example download link