Nehmen wir an, ich fordere eine große JSON-Datei an, die eine Liste vieler Objekte enthält. Ich möchte nicht, dass sie alle auf einmal im Gedächtnis bleiben, aber ich würde sie lieber einzeln lesen und verarbeiten. Also muss ich einen asynchronen System.IO.Stream
Stream in einen verwandeln IAsyncEnumerable<T>
. Wie verwende ich dazu die neue System.Text.Json
API?
private async IAsyncEnumerable<T> GetList<T>(Uri url, CancellationToken cancellationToken = default)
{
using (var httpResponse = await httpClient.GetAsync(url, cancellationToken))
{
using (var stream = await httpResponse.Content.ReadAsStreamAsync())
{
// Probably do something with JsonSerializer.DeserializeAsync here without serializing the entire thing in one go
}
}
}
c#
.net-core
.net-core-3.0
c#-8.0
system.text.json
Rick de Water
quelle
quelle
Utf8JsonReader
, bitte einen Blick auf einige Github haben Proben und bei bestehenden Thread als auchGetAsync
von selbst kehrt zurück, wenn die gesamte Antwort empfangen wird. Sie müssenSendAsync
stattdessen "HttpCompletionOption.ResponseContentRead" verwenden. Sobald Sie das haben, können Sie den JsonTextReader von JSON.NET verwenden . Die VerwendungSystem.Text.Json
hierfür ist nicht so einfach, wie dieses Problem zeigt . Die Funktionalität ist nicht verfügbar und die Implementierung in einer Low-Allocation mit Strukturen ist nicht trivialAntworten:
Ja, ein wirklich Streaming-JSON (De) Serializer wäre an so vielen Orten eine schöne Leistungsverbesserung.
Tut
System.Text.Json
dies derzeit leider nicht. Ich bin mir nicht sicher, ob es in Zukunft so sein wird - ich hoffe es! Die Streaming-Deserialisierung von JSON stellt sich als ziemlich herausfordernd heraus.Sie könnten vielleicht überprüfen, ob der extrem schnelle Utf8Json dies unterstützt.
Möglicherweise gibt es jedoch eine benutzerdefinierte Lösung für Ihre spezifische Situation, da Ihre Anforderungen die Schwierigkeit zu beschränken scheinen.
Die Idee ist, jeweils ein Element manuell aus dem Array zu lesen. Wir nutzen die Tatsache, dass jedes Element in der Liste für sich ein gültiges JSON-Objekt ist.
Sie können das
[
(für das erste Element) oder das,
(für jedes nächste Element) manuell überspringen . Dann ist es meiner Meinung nach am besten, mit .NET CoreUtf8JsonReader
zu bestimmen, wo das aktuelle Objekt endet, und die gescannten Bytes zuzuführenJsonDeserializer
.Auf diese Weise puffern Sie jeweils nur geringfügig über ein Objekt.
Und da es sich um Performance handelt, können Sie den Input von a erhalten
PipeReader
, während Sie gerade dabei sind. :-)quelle
TL; DR Es ist nicht trivial
Es sieht so aus, als hätte jemand bereits vollständigen Code für eine
Utf8JsonStreamReader
Struktur gepostet , die Puffer aus einem Stream liest und sie einem Utf8JsonRreader zuführt, wodurch eine einfache Deserialisierung mit ermöglicht wirdJsonSerializer.Deserialize<T>(ref newJsonReader, options);
. Der Code ist auch nicht trivial. Die verwandte Frage ist hier und die Antwort ist hier .Das reicht jedoch nicht aus -
HttpClient.GetAsync
wird erst zurückgegeben, nachdem die gesamte Antwort empfangen wurde, und im Wesentlichen alles im Speicher gepuffert.Um dies zu vermeiden, sollte HttpClient.GetAsync (Zeichenfolge, HttpCompletionOption) mit verwendet werden
HttpCompletionOption.ResponseHeadersRead
.Die Deserialisierungsschleife sollte auch das Stornierungs-Token überprüfen und entweder beenden oder werfen, wenn dies signalisiert wird. Andernfalls wird die Schleife fortgesetzt, bis der gesamte Stream empfangen und verarbeitet wurde.
Dieser Code basiert auf dem Beispiel der zugehörigen Antwort und verwendet
HttpCompletionOption.ResponseHeadersRead
und überprüft das Stornierungs-Token. Es kann JSON-Zeichenfolgen analysieren, die ein geeignetes Array von Elementen enthalten, z.Der erste Aufruf von
jsonStreamReader.Read()
bewegt sich zum Anfang des Arrays, während der zweite zum Anfang des ersten Objekts wechselt. Die Schleife selbst wird beendet, wenn das Ende des Arrays (]
) erkannt wird.JSON-Fragmente, AKA-Streaming JSON aka ... *
In Ereignis-Streaming- oder Protokollierungsszenarien ist es durchaus üblich, einzelne JSON-Objekte an eine Datei anzuhängen, ein Element pro Zeile, z.
Dies ist kein gültiges JSON- Dokument, aber die einzelnen Fragmente sind gültig. Dies hat mehrere Vorteile für Big Data- / hochkonkurrierende Szenarien. Das Hinzufügen eines neuen Ereignisses erfordert nur das Anhängen einer neuen Zeile an die Datei, nicht das Parsen und Neuerstellen der gesamten Datei. Die Verarbeitung , insbesondere die Parallelverarbeitung , ist aus zwei Gründen einfacher:
Verwenden eines StreamReader
Die allokative Möglichkeit, dies zu tun, besteht darin, einen TextReader zu verwenden, jeweils eine Zeile zu lesen und ihn mit JsonSerializer zu analysieren. Deserialize :
Das ist viel einfacher als der Code, der ein richtiges Array deserialisiert. Es gibt zwei Probleme:
ReadLineAsync
akzeptiert kein StornierungszeichenDies kann jedoch ausreichen, um zu versuchen, die
ReadOnlySpan<Byte>
von JsonSerializer benötigten Puffer zu erzeugen. Die Deserialisierung ist nicht trivial.Pipelines und SequenceReader
Um Zuordnungen zu vermeiden, müssen wir eine
ReadOnlySpan<byte>
aus dem Stream erhalten. Dazu müssen System.IO.Pipeline-Pipes und die SequenceReader- Struktur verwendet werden. Steve Gordons Eine Einführung in SequenceReader erklärt, wie diese Klasse zum Lesen von Daten aus einem Stream mithilfe von Trennzeichen verwendet werden kann.Leider
SequenceReader
handelt es sich um eine Ref-Struktur, was bedeutet, dass sie nicht in asynchronen oder lokalen Methoden verwendet werden kann. Deshalb schafft Steve Gordon in seinem Artikel eineMethode zum Lesen von Elementen aus einer ReadOnlySequence und zum Zurückgeben der Endposition, damit der PipeReader von dieser fortfahren kann. Leider möchten wir eine IEnumerable oder IAsyncEnumerable zurückgeben, und Iterator-Methoden mögen
in
oderout
Parameter auch nicht.Wir könnten die deserialisierten Elemente in einer Liste oder Warteschlange sammeln und als einzelnes Ergebnis zurückgeben, aber das würde weiterhin Listen, Puffer oder Knoten zuweisen und müssen warten, bis alle Elemente in einem Puffer deserialisiert sind, bevor wir zurückkehren:
Wir brauchen etwas , das sich wie eine Aufzählung verhält, ohne dass eine Iteratormethode erforderlich ist, mit Async arbeitet und nicht alles so puffert.
Hinzufügen von Kanälen zum Erstellen einer IAsyncEnumerable
ChannelReader.ReadAllAsync gibt eine IAsyncEnumerable zurück. Wir können einen ChannelReader von Methoden zurückgeben, die nicht als Iteratoren funktionieren konnten, und trotzdem einen Stream von Elementen ohne Caching erzeugen.
Wenn wir den Code von Steve Gordon an die Verwendung von Kanälen anpassen, erhalten wir die ReadItems (ChannelWriter ...) und
ReadLastItem
Methoden. Der erste liest jeweils ein Element bis zu einer neuen Zeile mitReadOnlySpan<byte> itemBytes
. Dies kann von verwendet werdenJsonSerializer.Deserialize
. WennReadItems
das Trennzeichen nicht gefunden werden kann, gibt es seine Position zurück, sodass der PipelineReader den nächsten Block aus dem Stream ziehen kann.Wenn wir den letzten Block erreichen und es kein anderes Trennzeichen gibt, liest ReadLastItem die verbleibenden Bytes und deserialisiert sie.
Der Code ist fast identisch mit dem von Steve Gordon. Anstatt an die Konsole zu schreiben, schreiben wir an den ChannelWriter.
Die
DeserializeToChannel<T>
Methode erstellt einen Pipeline-Reader über dem Stream, erstellt einen Kanal und startet eine Worker-Aufgabe, die Chunks analysiert und an den Kanal weiterleitet:ChannelReader.ReceiveAllAsync()
kann verwendet werden, um alle Artikel über Folgendes zu konsumierenIAsyncEnumerable<T>
:quelle
Es fühlt sich an, als müssten Sie Ihren eigenen Stream-Reader implementieren. Sie müssen die Bytes einzeln lesen und anhalten, sobald die Objektdefinition abgeschlossen ist. Es ist in der Tat ziemlich niedrig. Als solches werden Sie NICHT die gesamte Datei in den RAM laden, sondern den Teil übernehmen, mit dem Sie sich befassen. Scheint es eine Antwort zu sein?
quelle
Vielleicht könnten Sie
Newtonsoft.Json
Serializer verwenden? https://www.newtonsoft.com/json/help/html/Performance.htmSiehe insbesondere Abschnitt:
Bearbeiten
Sie können versuchen, Werte aus JsonTextReader zu deserialisieren, z
quelle
I don't want them to be in memory all at once, but I would rather read and process them one by one.
Die relevante Klasse in JSON.NET ist JsonTextReader.