Skip to content

Commit fd8785d

Browse files
ahsonkhanBillWagner
authored andcommitted
Create sync and async sample for using Utf8JsonReader. (dotnet#492)
* Create sync and async sample for using Utf8JsonReader. * Add reference to source of JSON data in comments. * Add cautionary comment for handling large JSON tokens (particularly strings) while streaming. * Make main async and address other feedback. * Clean-up csproj, comments, and variable name. * Move from csharp to core whats new in 30. * Move to core directory.
1 parent b6476e4 commit fd8785d

File tree

3 files changed

+194
-0
lines changed

3 files changed

+194
-0
lines changed

core/json/Program.cs

+184
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.IO;
4+
using System.Net.Http;
5+
using System.Text;
6+
using System.Text.Json;
7+
using System.Threading.Tasks;
8+
9+
namespace ReaderSample
10+
{
11+
class Program
12+
{
13+
private static readonly byte[] s_nameUtf8 = Encoding.UTF8.GetBytes("name");
14+
private static readonly byte[] s_universityOfUtf8 = Encoding.UTF8.GetBytes("University of");
15+
16+
public static async Task Main(string[] args)
17+
{
18+
// The JSON data used for the samples was borrowed from https://github.com/Hipo/university-domains-list
19+
// under the MIT License (MIT).
20+
21+
string outputMessage = SyncFileExample("world_universities_and_domains.json");
22+
Console.WriteLine("Reading JSON from file, sync: " + outputMessage);
23+
24+
outputMessage = await AsyncWebExample(@"http://universities.hipolabs.com/search?country=United%20States");
25+
Console.WriteLine("Reading JSON from web, async: " + outputMessage);
26+
outputMessage = await AsyncWebExample(@"http://universities.hipolabs.com/search?", worldWide: true);
27+
Console.WriteLine("Reading JSON from web, async: " + outputMessage);
28+
}
29+
30+
private static string SyncFileExample(string fileName)
31+
{
32+
// Follow the async web example if you want to read asynchronously from a FileStream instead.
33+
34+
ReadOnlySpan<byte> dataWorld = GetUtf8JsonFromDisk(fileName);
35+
(int count, int total) = CountUniversityOf(dataWorld);
36+
double ratio = (double)count / total;
37+
return $"{count} out of {total} universities worldwide have names starting with 'University of' (i.e. {ratio.ToString("#.##%")})!";
38+
}
39+
40+
private static ReadOnlySpan<byte> GetUtf8JsonFromDisk(string fileName)
41+
{
42+
// Read as UTF-16 and transcode to UTF-8 to return as a Span<byte>
43+
// For example:
44+
// string jsonString = File.ReadAllText(fileName);
45+
// return Encoding.UTF8.GetBytes(jsonString);
46+
47+
// OR ReadAllBytes if the file encoding is known to be UTF-8 and skip the encoding step:
48+
byte[] jsonBytes = File.ReadAllBytes(fileName);
49+
return jsonBytes;
50+
}
51+
52+
public static (int count, int total) CountUniversityOf(ReadOnlySpan<byte> dataUtf8)
53+
{
54+
int count = 0;
55+
int total = 0;
56+
57+
var json = new Utf8JsonReader(dataUtf8, isFinalBlock: true, state: default);
58+
59+
while (json.Read())
60+
{
61+
JsonTokenType tokenType = json.TokenType;
62+
63+
switch (tokenType)
64+
{
65+
case JsonTokenType.StartObject:
66+
total++;
67+
break;
68+
case JsonTokenType.PropertyName:
69+
if (json.ValueSpan.SequenceEqual(s_nameUtf8))
70+
{
71+
bool result = json.Read();
72+
73+
Debug.Assert(result); // Assume valid JSON
74+
Debug.Assert(json.TokenType == JsonTokenType.String); // Assume known, valid JSON schema
75+
76+
if (json.ValueSpan.StartsWith(s_universityOfUtf8))
77+
{
78+
count++;
79+
}
80+
}
81+
break;
82+
}
83+
}
84+
return (count, total);
85+
}
86+
87+
private static async Task<string> AsyncWebExample(string url, bool worldWide = false)
88+
{
89+
using (var client = new HttpClient())
90+
{
91+
using (Stream stream = await client.GetStreamAsync(url))
92+
{
93+
(int count, int total) = await ReadJsonFromStreamUsingSpan(stream);
94+
95+
double ratio = (double)count / total;
96+
string percentage = ratio.ToString("#.##%");
97+
string outputMessage = worldWide ?
98+
$"{count} out of {total} universities worldwide have names starting with 'University of' (i.e. {percentage})!" :
99+
$"{count} out of {total} American universities have names starting with 'University of' (i.e. {percentage})!";
100+
101+
return outputMessage;
102+
}
103+
}
104+
}
105+
106+
public static async Task<(int count, int total)> ReadJsonFromStreamUsingSpan(Stream stream)
107+
{
108+
// Assumes all JSON strings in the payload are small (say < 500 bytes)
109+
var buffer = new byte[1_024];
110+
int count = 0;
111+
int total = 0;
112+
113+
JsonReaderState state = default;
114+
int leftOver = 0;
115+
int partialCount = 0;
116+
int partialTotalCount = 0;
117+
bool foundName = false;
118+
119+
while (true)
120+
{
121+
// The Memory<byte> ReadAsync overload returns ValueTask which is allocation-free
122+
// if the operation completes synchronously
123+
int dataLength = await stream.ReadAsync(buffer.AsMemory(leftOver, buffer.Length - leftOver));
124+
int dataSize = dataLength + leftOver;
125+
bool isFinalBlock = dataSize == 0;
126+
(state, partialCount, partialTotalCount) = PartialCountUniversityOf(buffer.AsSpan(0, dataSize), isFinalBlock, ref foundName, state);
127+
128+
// Based on your scenario and input data, you may need to grow your buffer here
129+
// It's possible that leftOver == dataSize (if a JSON token is too large)
130+
// so you need to resize and read more than 1_024 bytes.
131+
leftOver = dataSize - (int)state.BytesConsumed;
132+
if (leftOver != 0)
133+
{
134+
buffer.AsSpan(dataSize - leftOver, leftOver).CopyTo(buffer);
135+
}
136+
137+
count += partialCount;
138+
total += partialTotalCount;
139+
140+
if (isFinalBlock)
141+
{
142+
break;
143+
}
144+
}
145+
146+
return (count, total);
147+
}
148+
149+
public static (JsonReaderState state, int count, int total) PartialCountUniversityOf(ReadOnlySpan<byte> dataUtf8, bool isFinalBlock, ref bool foundName, JsonReaderState state)
150+
{
151+
int count = 0;
152+
int total = 0;
153+
154+
var json = new Utf8JsonReader(dataUtf8, isFinalBlock, state);
155+
156+
while (json.Read())
157+
{
158+
JsonTokenType tokenType = json.TokenType;
159+
160+
switch (tokenType)
161+
{
162+
case JsonTokenType.StartObject:
163+
total++;
164+
break;
165+
case JsonTokenType.PropertyName:
166+
if (json.ValueSpan.SequenceEqual(s_nameUtf8))
167+
{
168+
foundName = true;
169+
}
170+
break;
171+
case JsonTokenType.String:
172+
if (foundName && json.ValueSpan.StartsWith(s_universityOfUtf8))
173+
{
174+
count++;
175+
}
176+
foundName = false;
177+
break;
178+
}
179+
}
180+
181+
return (json.CurrentState, count, total);
182+
}
183+
}
184+
}

core/json/ReaderSample.csproj

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<OutputType>Exe</OutputType>
5+
<TargetFramework>netcoreapp3.0</TargetFramework>
6+
<LangVersion>latest</LangVersion>
7+
</PropertyGroup>
8+
9+
</Project>

core/json/world_universities_and_domains.json

+1
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)