CSharp · Console
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization.Formatters;
using System.IO;
using System.Data;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Reflection;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Threading;
using System.Runtime.CompilerServices;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using NUnit.Framework; // Throws NUnit.Framework.AssertionException
//https://stackoverflow.com/questions/72451669/deserializing-to-asyncenumerable-using-newtonsoft-json
#nullable enable
public static partial class JsonExtensions
{
/// <summary>
/// Asynchronously load and synchronously deserialize values from a stream containing a JSON array. The root object of the JSON stream must in fact be an array, or an exception is thrown
/// </summary>
public static async IAsyncEnumerable<T?> DeserializeAsyncEnumerable<T>(Stream stream, JsonSerializerSettings? settings = default, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
var serializer = JsonSerializer.CreateDefault(settings);
var loadSettings = new JsonLoadSettings { LineInfoHandling = LineInfoHandling.Ignore }; // For performance do not load line info.
// StreamReader and JsonTextReader do not implement IAsyncDisposable so let the caller dispose the stream.
using (var textReader = new StreamReader(stream, leaveOpen : true))
using (var reader = new JsonTextReader(textReader) { CloseInput = false })
{
await foreach (var token in LoadAsyncEnumerable(reader, loadSettings, cancellationToken ).ConfigureAwait(false))
yield return token.ToObject<T>(serializer);
}
}
/// <summary>
/// Asynchronously load and return JToken values from a stream containing a JSON array. The root object of the JSON stream must in fact be an array, or an exception is thrown
/// </summary>
public static async IAsyncEnumerable<JToken> LoadJTokenAsyncEnumerable(Stream stream, JsonLoadSettings? settings = default, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// StreamReader and JsonTextReader do not implement IAsyncDisposable so let the caller dispose the stream.
using (var textReader = new StreamReader(stream, leaveOpen : true))
using (var reader = new JsonTextReader(textReader) { CloseInput = false })
{
await foreach (var token in LoadAsyncEnumerable(reader, settings, cancellationToken).ConfigureAwait(false))
yield return token;
}
}
/// <summary>
/// Asynchronously load and return JToken values from a stream containing a JSON array. The root object of the JSON stream must in fact be an array, or an exception is thrown
/// </summary>
public static async IAsyncEnumerable<JToken> LoadAsyncEnumerable(JsonTextReader reader, JsonLoadSettings? settings = default, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
(await reader.MoveToContentAndAssertAsync().ConfigureAwait(false)).AssertTokenType(JsonToken.StartArray);
cancellationToken.ThrowIfCancellationRequested();
while ((await reader.ReadToContentAndAssert(cancellationToken).ConfigureAwait(false)).TokenType != JsonToken.EndArray)
{
cancellationToken.ThrowIfCancellationRequested();
yield return await JToken.LoadAsync(reader, settings, cancellationToken).ConfigureAwait(false);
}
cancellationToken.ThrowIfCancellationRequested();
}
public static JsonReader AssertTokenType(this JsonReader reader, JsonToken tokenType) =>
reader.TokenType == tokenType ? reader : throw new JsonSerializationException(string.Format("Unexpected token {0}, expected {1}", reader.TokenType, tokenType));
public static async Task<JsonReader> ReadToContentAndAssert(this JsonReader reader, CancellationToken cancellationToken = default) =>
await (await reader.ReadAndAssertAsync(cancellationToken).ConfigureAwait(false)).MoveToContentAndAssertAsync(cancellationToken).ConfigureAwait(false);
public static async Task<JsonReader> MoveToContentAndAssertAsync(this JsonReader reader, CancellationToken cancellationToken = default)
{
if (reader == null)
throw new ArgumentNullException();
if (reader.TokenType == JsonToken.None) // Skip past beginning of stream.
await reader.ReadAndAssertAsync(cancellationToken).ConfigureAwait(false);
while (reader.TokenType == JsonToken.Comment) // Skip past comments.
await reader.ReadAndAssertAsync(cancellationToken).ConfigureAwait(false);
return reader;
}
public static async Task<JsonReader> ReadAndAssertAsync(this JsonReader reader, CancellationToken cancellationToken = default)
{
if (reader == null)
throw new ArgumentNullException();
if (!await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
throw new JsonReaderException("Unexpected end of JSON stream.");
return reader;
}
}
// Test code
public class SurrogateStream : Stream
{
Stream? baseStream;
Stream BaseStream => baseStream ?? throw new ObjectDisposedException(nameof(BaseStream));
public SurrogateStream(Stream baseStream) => this.baseStream = baseStream ?? throw new ArgumentNullException(nameof(baseStream));
public override long Length => BaseStream.Length;
public override long Position { get => BaseStream.Position; set => BaseStream.Position = value; }
public override bool CanRead => BaseStream.CanRead;
public override bool CanWrite => BaseStream.CanWrite;
public override bool CanSeek => BaseStream.CanSeek;
public override bool CanTimeout => BaseStream.CanTimeout;
public override IAsyncResult BeginRead (byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => BaseStream.BeginRead(buffer, offset, count, callback, state);
public override IAsyncResult BeginWrite (byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) => BaseStream.BeginWrite(buffer, offset, count, callback, state);
//public override void Close () => BaseStream.Close();
public override void CopyTo (System.IO.Stream destination, int bufferSize) => BaseStream.CopyTo(destination, bufferSize);
public override Task CopyToAsync (System.IO.Stream destination, int bufferSize, System.Threading.CancellationToken cancellationToken) => BaseStream.CopyToAsync(destination, bufferSize, cancellationToken);
//public override System.Runtime.Remoting.ObjRef CreateObjRef (Type requestedType) => BaseStream.CreateObjRef(requestedType);
protected override void Dispose (bool disposing)
{
try
{
if (disposing)
Interlocked.Exchange(ref baseStream, null)?.Dispose();
}
finally
{
base.Dispose(disposing);
}
}
public override ValueTask DisposeAsync() => Interlocked.Exchange(ref baseStream, null)?.DisposeAsync() ?? ValueTask.CompletedTask;
public override int EndRead (IAsyncResult asyncResult) => BaseStream.EndRead(asyncResult);
public override void EndWrite (IAsyncResult asyncResult) => BaseStream.EndWrite(asyncResult);
public override void Flush() => BaseStream.Flush();
public override Task FlushAsync (CancellationToken cancellationToken) => BaseStream.FlushAsync(cancellationToken);
public override int Read (byte[] buffer, int offset, int count) => BaseStream.Read(buffer, offset, count);
public override int Read (Span<byte> buffer) => BaseStream.Read(buffer);
public override Task<int> ReadAsync (byte[] buffer, int offset, int count, CancellationToken cancellationToken) => BaseStream.ReadAsync(buffer, offset, count, cancellationToken);
public override ValueTask<int> ReadAsync (Memory<byte> buffer, System.Threading.CancellationToken cancellationToken = default) => BaseStream.ReadAsync(buffer, cancellationToken);
public override int ReadByte () => BaseStream.ReadByte();
public override long Seek (long offset, System.IO.SeekOrigin origin) => BaseStream.Seek(offset, origin);
public override void SetLength (long value) => BaseStream.SetLength(value);
public override void Write (byte[] buffer, int offset, int count) => BaseStream.Write(buffer, offset, count);
public override void Write (ReadOnlySpan<byte> buffer) => BaseStream.Write(buffer);
public override System.Threading.Tasks.Task WriteAsync (byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) => BaseStream.WriteAsync(buffer, offset, count, cancellationToken);
public override System.Threading.Tasks.ValueTask WriteAsync (ReadOnlyMemory<byte> buffer, System.Threading.CancellationToken cancellationToken = default) => BaseStream.WriteAsync(buffer, cancellationToken);
public override void WriteByte (byte value) => BaseStream.WriteByte(value);
}
public class AsyncOnlySurogateStream : SurrogateStream
{
public AsyncOnlySurogateStream(Stream baseStream) : base(baseStream) { }
public override void CopyTo (System.IO.Stream destination, int bufferSize) => throw new InvalidOperationException(string.Format("Non async method {0} called", System.Reflection.MethodBase.GetCurrentMethod()?.Name));
protected override void Dispose (bool disposing)
{
base.Dispose(disposing);
if (disposing)
{
throw new InvalidOperationException(string.Format("Non async method {0} called", System.Reflection.MethodBase.GetCurrentMethod()?.Name));
}
}
public override void Flush() => throw new InvalidOperationException(string.Format("Non async method {0} called", System.Reflection.MethodBase.GetCurrentMethod()?.Name));
public override int Read (byte[] buffer, int offset, int count) => throw new InvalidOperationException(string.Format("Non async method {0} called", System.Reflection.MethodBase.GetCurrentMethod()?.Name));
public override int Read (Span<byte> buffer) => throw new InvalidOperationException(string.Format("Non async method {0} called", System.Reflection.MethodBase.GetCurrentMethod()?.Name));
public override int ReadByte () => throw new InvalidOperationException(string.Format("Non async method {0} called", System.Reflection.MethodBase.GetCurrentMethod()?.Name));
public override void Write (byte[] buffer, int offset, int count) => throw new InvalidOperationException(string.Format("Non async method {0} called", System.Reflection.MethodBase.GetCurrentMethod()?.Name));
public override void Write (ReadOnlySpan<byte> buffer) => throw new InvalidOperationException(string.Format("Non async method {0} called", System.Reflection.MethodBase.GetCurrentMethod()?.Name));
public override void WriteByte (byte value) => throw new InvalidOperationException(string.Format("Non async method {0} called", System.Reflection.MethodBase.GetCurrentMethod()?.Name));
}
public class Model
{
public int Id { get; set; }
public string? Name { get; set; }
}
class TestClass
{
public static async Task Test()
{
await TestDeserializeAsync();
Console.WriteLine();
await TestLoadJTokenAsync();
Assert.ThrowsAsync<InvalidOperationException>(() => TestReadAsJsonNonAsyncFail());
Assert.ThrowsAsync<InvalidOperationException>(() => TestReadAsTextNonAsyncFail());
Assert.DoesNotThrowAsync(() => TestReadAsTextAsync());
Assert.DoesNotThrowAsync(() => TestReadAsJsonAsync());
}
public static async Task TestDeserializeAsync(CancellationToken cancellationToken = default)
{
var inputs = Enumerable.Range(0,12).Select(i => new Model { Id = i, Name = $"Model {i}" }).ToList();
using var innerStream = new MemoryStream();
using (var textWriter = new StreamWriter(innerStream, leaveOpen : true))
using (var writer = new JsonTextWriter(textWriter) { CloseOutput = false })
{
JsonSerializer.CreateDefault().Serialize(writer, inputs);
}
innerStream.Position = 0;
await using var stream = new AsyncOnlySurogateStream(innerStream);
await foreach (var token in JsonExtensions.LoadJTokenAsyncEnumerable(stream, cancellationToken : cancellationToken))
{
Console.WriteLine(token.ToString(Formatting.None));
}
}
public static async Task TestLoadJTokenAsync(CancellationToken cancellationToken = default)
{
var inputs = Enumerable.Range(0,10).Select(i => new Model { Id = i, Name = $"Token {i}" }).ToList();
using var innerStream = new MemoryStream();
using (var textWriter = new StreamWriter(innerStream, leaveOpen : true))
using (var writer = new JsonTextWriter(textWriter) { CloseOutput = false })
{
JsonSerializer.CreateDefault().Serialize(writer, inputs);
}
innerStream.Position = 0;
await using var stream = new AsyncOnlySurogateStream(innerStream);
await foreach (var model in JsonExtensions.DeserializeAsyncEnumerable<Model>(stream, cancellationToken : cancellationToken))
{
// Process each item somehow
Console.WriteLine(JsonConvert.SerializeObject(model, Formatting.None));
}
}
public static async Task TestReadAsTextAsync()
{
using var innerStream = new MemoryStream(Encoding.UTF8.GetBytes(GetJson()));
await using (var stream = new AsyncOnlySurogateStream(innerStream))
{
using (var textReader = new StreamReader(stream, leaveOpen : true))
{
string? line;
while ((line = await textReader.ReadLineAsync()) != null)
;
}
}
}
public static async Task TestReadAsTextNonAsyncFail()
{
using var innerStream = new MemoryStream(Encoding.UTF8.GetBytes(GetJson()));
await using (var stream = new AsyncOnlySurogateStream(innerStream))
{
using (var textReader = new StreamReader(stream, leaveOpen : false))
{
string? line;
while ((line = await textReader.ReadLineAsync()) != null)
;
}
}
}
public static async Task TestReadAsJsonAsync()
{
var inputs = Enumerable.Range(0,12000).Select(i => new Model { Id = i, Name = $"Model {i}" }).ToList();
using var innerStream = new MemoryStream();
using (var textWriter = new StreamWriter(innerStream, leaveOpen : true))
using (var writer = new JsonTextWriter(textWriter) { CloseOutput = false })
{
JsonSerializer.CreateDefault().Serialize(writer, inputs);
}
Console.WriteLine($"\ninnerStream.Length = {innerStream.Length}");
innerStream.Position = 0;
await using (var stream = new AsyncOnlySurogateStream(innerStream))
{
using (var textReader = new StreamReader(stream, leaveOpen : true))
{
using (var reader = new JsonTextReader(textReader) { CloseInput = false })
{
while (await reader.ReadAsync())
;
}
}
}
}
public static async Task TestReadAsJsonNonAsyncFail()
{
using var innerStream = new MemoryStream(Encoding.UTF8.GetBytes(GetJson()));
await using (var stream = new AsyncOnlySurogateStream(innerStream))
{
using (var textReader = new StreamReader(stream, leaveOpen : true))
{
using (var reader = new JsonTextReader(textReader) { CloseInput = false })
{
while (reader.Read())
{
Console.WriteLine($"TokenType={reader.TokenType}, Value={reader.Value}");
}
}
}
}
}
static string GetJson() => @"[{
""glossary"": {
""title"": ""example glossary"",
""GlossDiv"": {
""title"": ""S"",
""GlossList"": {
""GlossEntry"": {
""ID"": ""SGML"",
""SortAs"": ""SGML"",
""GlossTerm"": ""Standard Generalized Markup Language"",
""Acronym"": ""SGML"",
""Abbrev"": ""ISO 8879:1986"",
""GlossDef"": {
""para"": ""A meta-markup language, used to create markup languages such as DocBook."",
""GlossSeeAlso"": [""GML"", ""XML""]
},
""GlossSee"": ""markup""
}
}
}
}
},{
""glossary"": {
""title"": ""example glossary"",
""GlossDiv"": {
""title"": ""S"",
""GlossList"": {
}
}
}
},{
""glossary"": {
""title"": ""example glossary"",
}
},
null,true,false,1,2,""hello"",
]
";
}
public class Program
{
public static async Task Main(string[] args)
{
Console.WriteLine("Environment version: {0} ({1})", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , Environment.Version);
Console.WriteLine("{0} version: {1}", typeof(JsonSerializer).Assembly.GetName().Name, typeof(JsonSerializer).Assembly.FullName);
Console.WriteLine();
try
{
await TestClass.Test();
}
catch (Exception ex)
{
Console.WriteLine("Failed with unhandled exception: ");
Console.WriteLine(ex);
throw;
}
}
}