Mert Özen Her Satırda Daha İleri
Backend .NET'te I/O İşlemleri — #05

System.Text.Json Derinlemesine 2: Source Generation, JsonNode/Utf8JsonReader ve Büyük JSON'u Streaming İşlemek

Mert Özen 14 Kas 2026 13 dk 1 görüntülenme
System.Text.Json Derinlemesine 2: Source Generation, JsonNode/Utf8JsonReader ve Büyük JSON'u Streaming İşlemek

Büyük bir JSON export'unu List<T>'ye deserialize etmek heap'i dosya boyutunun iki katının üstüne çıkarır; DeserializeAsyncEnumerable ile aynı iş sabit ve küçük bir bellekle biter. 1 milyon siparişlik Bookstore export'unda bunu ölçüyor, source generation'ın başlangıç ve fast path kazancını, Native AOT'taki davranışını, JsonDocument/JsonNode/Utf8JsonReader seçimini, JsonSchemaExporter'ı ve Newtonsoft.Json'dan geçişi gösteriyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 (System.Text.Json 10) ve BenchmarkDotNet 0.15.8 ile derlenip denenmiştir.

Önce Tahmin Et

Bookstore'un (sitedeki serilerin ortak örnek uygulaması, bir online kitapçı) muhasebe ekibi her ay 1 milyon siparişlik, 348 MB'lık bir JSON export'u alıyor. Toplam ciroyu hesaplayan kod şöyle:

List<Order> all = (await JsonSerializer.DeserializeAsync<List<Order>>(file))!;
decimal revenue = all.Sum(o => o.Total);

Bu işlem sırasında managed heap en fazla kaç MB'a çıkar: 348 MB'ın altında mı, civarında mı, iki katından fazla mı?

İkinci soru: aynı kodu Native AOT (Ahead-of-Time; uygulamayı yayınlarken doğrudan makine koduna derleyen mod) ile yayınlanan bir servise taşıyorsun. JsonSerializer.Serialize(order) satırı derlenir mi, çalışır mı?

Üçüncüsü: source generation ile üretilen bir JsonSerializerContext, uygulama ısındıktan sonra her serialization'ı reflection'a göre ne kadar hızlandırır?

Bir önceki yazıda STJ'nin (System.Text.Json kütüphanesinin kısa adı) ayarlarına ve model kurallarına bakmıştık. Bu yazıda performans ve ölçek tarafına geçiyoruz. Örnekler serinin repo'sunda Bookstore.IO.Json, Bookstore.Api ve Bookstore.IO.Benchmarks projelerinde:

dotnet run -c Release --project src/Bookstore.IO.Json -- export 1000000
dotnet run -c Release --project src/Bookstore.IO.Json -- summary stream

Source Generation: JsonSerializerContext ve [JsonSerializable]

STJ bir tipi ilk kez gördüğünde onu reflection ile inceler: property'leri, constructor'ı, attribute'ları bulur ve bunlardan bir metadata nesnesi (JsonTypeInfo) üretir. Bu iş her process'te, her tip için bir kez yapılır. Source generation aynı işi derleme sırasında yapar; bir partial class tanımlarsın, source generator gerekli kodu üretir:

[JsonSourceGenerationOptions(JsonSerializerDefaults.Web, UseStringEnumConverter = true)]
[JsonSerializable(typeof(Order))]
[JsonSerializable(typeof(List<Order>))]
[JsonSerializable(typeof(IAsyncEnumerable<Order>))]
[JsonSerializable(typeof(ExportSummary))]
public partial class BookstoreJsonContext : JsonSerializerContext;

Kullanırken options yerine context'in ürettiği JsonTypeInfo'yu verirsin: JsonSerializer.Serialize(order, BookstoreJsonContext.Default.Order). Order'ın içindeki OrderItem ve OrderStatus gibi tipler otomatik olarak dahil edilir; yalnız kök tipleri listelemen yeterli. Source generator'ların genel çalışma mantığı C# İleri Seviye serisinin Source Generator yazısında.

İlk çağrının maliyetini ölçmek için her modu ayrı bir process'te, beşer kez çalıştırdık:

reflection first call:   19.8 ms,   246,264 bytes, 438 chars
sourcegen  first call:   15.3 ms,   103,608 bytes, 438 chars
reflection first call:   19.5 ms,   245,880 bytes, 438 chars
sourcegen  first call:   15.1 ms,   103,608 bytes, 438 chars
reflection first call:   19.7 ms,   250,336 bytes, 438 chars
sourcegen  first call:   15.0 ms,   103,608 bytes, 438 chars

Aynı çıktı için ilk çağrı yaklaşık 4,5 ms daha kısa ve 140 KB daha az allocation yapıyor. Tek bir tip için küçük bir rakam; ama yüzlerce DTO'su (Data Transfer Object; katmanlar arasında veri taşıyan basit tip) olan bir API'de, her yeni pod'un ilk isteklerinde ve kısa ömürlü serverless fonksiyonlarda bu maliyet her tip için ayrı ayrı ödenir. Kalan 15 ms'nin büyük kısmı JIT (Just-In-Time; method'ları ilk çağrıldıklarında makine koduna çeviren derleyici) derlemesi; JIT'in de olmadığı Native AOT'ta başlangıç farkı daha belirgin hâle gelir.

Native AOT ve trimming

İkinci sorunun cevabı. PublishAot açık bir projede reflection tabanlı çağrı derlenir, ama iki uyarı verir:

warning IL2026: Using member 'System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. JSON serialization and deserialization might require types that cannot be statically analyzed. Use the overload that takes a JsonTypeInfo or JsonSerializerContext, or make sure all of the required types are preserved.
warning IL3050: Using member 'System.Text.Json.JsonSerializer.Serialize<TValue>(TValue, JsonSerializerOptions)' which has 'RequiresDynamicCodeAttribute' can break functionality when AOT compiling. JSON serialization and deserialization might require types that cannot be statically analyzed and might need runtime code generation. Use System.Text.Json source generation for native AOT applications.

Trimming ya da AOT ile yayınladığında SDK, JsonSerializerIsReflectionEnabledByDefault ayarını false yapar. O durumda ne olduğunu, aynı switch'i bir console uygulamasında elle kapatarak gördük:

AppContext.SetSwitch("System.Text.Json.JsonSerializer.IsReflectionEnabledByDefault", false);
Reflection: InvalidOperationException: Reflection-based serialization has been disabled for this application. Either use the source generator APIs or explicitly configure the 'JsonSerializerOptions.TypeInfoResolver' property.
Source gen: {"orderNumber":"BS-0000001","customerName":"Linus","createdA...

Uyarıları görmezden gelirsen hata derlemede değil, çalışma zamanında ilk JSON çağrısında gelir. AOT'a geçecek projede bu uyarıları hata olarak ele al.

Metadata ve Serialization-Optimization Modu

Source generator iki şey üretebilir. Metadata modu, reflection'ın çalışma zamanında çıkardığı JsonTypeInfo'yu derleme zamanında üretir; serialization ve deserialization ikisi de bunu kullanır. Serialization-optimization (fast path) modu ise doğrudan Utf8JsonWriter'a yazan özel bir method üretir; yalnız senkron serialization'da kullanılır ve her ayarı desteklemez (custom converter'lar, ReferenceHandler, encoder ayarı gibi). Varsayılan olarak ikisi birden üretilir ve fast path kullanılamadığında metadata'ya düşülür. Üçüncü sorunun cevabı için 1.000 siparişi üç yolla serialize ettik:

[JsonSourceGenerationOptions(JsonSerializerDefaults.Web, GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(List<Order>))]
public partial class MetadataOnlyContext : JsonSerializerContext;

[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase, GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(List<Order>))]
public partial class FastPathContext : JsonSerializerContext;

[Benchmark]
public void SourceGenFastPath()
{
    _writer.Reset();
    JsonSerializer.Serialize(_writer, _orders, FastPathContext.Default.ListOrder);
    _writer.Flush();
}
| Method            | Mean     | Error   | StdDev  | Ratio | RatioSD | Gen0   | Allocated | Alloc Ratio |
|------------------ |---------:|--------:|--------:|------:|--------:|-------:|----------:|------------:|
| Reflection        | 389.5 us | 6.73 us | 7.76 us |  1.00 |    0.03 | 9.2773 |   80312 B |        1.00 |
| SourceGenMetadata | 363.8 us | 7.25 us | 8.91 us |  0.93 |    0.03 | 9.2773 |   80312 B |        1.00 |
| SourceGenFastPath | 280.9 us | 2.92 us | 2.73 us |  0.72 |    0.02 |      - |         - |        0.00 |

Isınmış bir uygulamada metadata modu reflection'dan anlamlı şekilde hızlı değil; aynı benchmark'ın başka bir koşusunda oran 1,12 çıkmıştı, yani fark ölçüm gürültüsü içinde. Bu beklenen bir sonuç: reflection ile üretilen metadata da ilk çağrıdan sonra cache'leniyor. Metadata modunun kazancı başlangıçta ve AOT uyumluluğunda. Fast path ise gerçekten fark yaratıyor: süre yüzde 28 kısalıyor ve allocation sıfıra iniyor.

Bunu denerken bir tuzağa da düştük. Yalnız Serialization modunu seçip aynı context'i Stream alan overload ile kullandığımızda çağrı fast path'e giremedi, düşeceği metadata da olmadığı için şu hatayı aldık:

System.InvalidOperationException: TypeInfoResolver 'Bookstore.IO.Benchmarks.FastPathContext' did not provide property metadata for type 'Bookstore.Ordering.Order'.

Pratik sonuç: modu değiştirmek için ölçülmüş bir sebebin yoksa varsayılanda (ikisi birden) kal. Fast path'ten yararlanmak istiyorsan, sıcak yoldaki çağrının senkron olduğunu ve desteklenmeyen bir ayar ya da attribute (ör. [JsonConverter]) kullanmadığını benchmark ile doğrula. Context'teki UseStringEnumConverter ayarının fast path'i kapatmadığını ayrıca denedik; şüphen olan her ayarı aynı şekilde yalnız Serialization modlu küçük bir context ile test edebilirsin.

ASP.NET Core'da Source Generation

Minimal API'lerde context'i ConfigureHttpJsonOptions ile resolver zincirinin başına ekliyoruz:

var builder = WebApplication.CreateSlimBuilder(args);

builder.Services.ConfigureHttpJsonOptions(options =>
{
    // Önce source-gen metadata; context'in tanımadığı tipler zincirdeki sonraki resolver'a düşer.
    options.SerializerOptions.TypeInfoResolverChain.Insert(0, BookstoreJsonContext.Default);
});

var app = builder.Build();

// JSON array'i stream eder: her sipariş üretildikçe yazılır, hiçbir şey buffer'lanmaz.
app.MapGet("/api/orders/export", (int count) => ExportOrders(count));

static async IAsyncEnumerable<Order> ExportOrders(int count)
{
    foreach (var order in SampleOrders.Generate(count))
    {
        yield return order;
    }
    await Task.CompletedTask;
}
curl -i "http://localhost:5080/api/orders/export?count=2"
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Server: Kestrel
Transfer-Encoding: chunked

[{"orderNumber":"BS-0000001","customerName":"Linus","createdAtUtc":"2026-11-01T00:00:07Z","status":"Pending","currency":"TRY","items":[{"isbn":"9780141439587","title":"Emma","quantity":1,"unitPrice":189.50,"lineTotal":189.50},...],"total":1116.40},{"orderNumber":"BS-0000002",...}]

Transfer-Encoding: chunked, yanıtın uzunluğu bilinmeden parça parça gönderildiğini gösteriyor. Endpoint IAsyncEnumerable<Order> döndüğü için STJ her siparişi geldikçe response'a yazıyor; 200.000 siparişlik (yaklaşık 70 MB) bir istekte de sunucu bütün listeyi bellekte toplamıyor. TypeInfoResolverChain.Insert(0, ...) kullanmamızın sebebi, context'e eklemeyi unuttuğun bir tipin hata vermek yerine reflection'a düşmesi. AOT'ta reflection resolver zincirde olmayacağı için o tip yine hata verir; bu yüzden AOT'a geçerken bütün endpoint tiplerini context'e ekle.

DOM Seçenekleri: JsonDocument, JsonElement ve JsonNode

Her JSON için bir C# tipi tanımlamak gerekmez. Bir ödeme sağlayıcısından gelen webhook'tan iki alan okuyacaksan ya da JSON'u değiştirip başka bir yere iletiyorsan DOM (Document Object Model; dokümanı bellekte bir ağaç olarak tutan model) API'leri daha uygun:

JsonElement kept;
using (JsonDocument doc = JsonDocument.Parse(webhook))
{
    JsonElement root = doc.RootElement;
    Console.WriteLine($"JsonDocument: {root.GetProperty("event").GetString()} for {root.GetProperty("orderNumber").GetString()}");
    kept = root;
}
Console.WriteLine(kept.GetProperty("event").GetString()); // ObjectDisposedException

JsonNode node = JsonNode.Parse(webhook)!;
node["attempt"] = node["attempt"]!.GetValue<int>() + 1;
node["receivedAtUtc"] = "2026-11-14T09:31:00Z";
node.AsObject().Remove("amount");
Console.WriteLine($"JsonNode: {node.ToJsonString()}");
JsonDocument: payment.captured for BS-0000042
After Dispose: ObjectDisposedException
JsonNode: {"event":"payment.captured","orderNumber":"BS-0000042","attempt":2,"receivedAtUtc":"2026-11-14T09:31:00Z"}

JsonDocument salt okunurdur ve hızlıdır: JSON'u pool'dan kiraladığı bir buffer'da tutar, JsonElement'ler bu buffer'a bakan hafif struct'lardır. Bedeli, dispose etmenin zorunlu olması ve JsonElement'in document'tan uzun yaşayamaması. Elemanı using bloğunun dışına taşıman gerekiyorsa Clone() ile kopyala. JsonNode (JsonObject, JsonArray, JsonValue) ise değiştirilebilir bir ağaçtır; alan ekleyip silebilir, değerleri değiştirebilirsin. Daha fazla allocation yapar ama dispose gerektirmez. Kısaca: yalnız okuyacaksan JsonDocument, değiştireceksen JsonNode, iş kuralları olan bir modelin varsa tipli deserialization.

Büyük JSON'u Streaming İşlemek

Birinci soruya dönelim. 1 milyon siparişi bir JSON array'i olarak yazdık; IAsyncEnumerable<Order> verdiğimiz için yazma tarafında da liste hiç oluşmadı:

public static async Task WriteAsync(Stream destination, IAsyncEnumerable<Order> orders, CancellationToken ct = default) =>
    await JsonSerializer.SerializeAsync(destination, orders, BookstoreJsonContext.Default.IAsyncEnumerableOrder, ct);
Wrote 1,000,000 orders, 348,070,221 bytes in 1,888 ms, peak heap 6,852,336 bytes

Sonra aynı dosyayı üç yolla okuduk ve her process'te GC'nin (garbage collector; kullanılmayan belleği geri toplayan runtime bileşeni) gördüğü en büyük heap boyutunu yazdırdık:

public static async Task<ExportSummary> SummarizeAsync(Stream source, CancellationToken ct = default)
{
    long count = 0;
    decimal revenue = 0;
    var customers = new HashSet<string>();
    await foreach (Order? order in JsonSerializer.DeserializeAsyncEnumerable(source, BookstoreJsonContext.Default.Order, ct))
    {
        count++;
        revenue += order!.Total;
        customers.Add(order.CustomerName);
    }
    return new ExportSummary(count, revenue, customers.Count);
}
list    1,000,000 orders, 888,278,668.60 TRY in 2,770 ms, peak heap 835,936,128 bytes, allocated 960,005,904 bytes
stream  1,000,000 orders, 888,278,668.60 TRY, 6 customers in 1,643 ms, peak heap 7,937,632 bytes, allocated 943,200,488 bytes
reader  888,278,668.60 TRY in 549 ms, peak heap 127,920 bytes, allocated 127,632 bytes

Cevap: List<Order> ile heap 836 MB'a çıktı, dosyanın iki katından fazla. JSON'daki her string bir .NET string'ine (UTF-16, yani karakter başına 2 byte), her sipariş bir nesne grafiğine dönüşüyor. DeserializeAsyncEnumerable ile aynı iş 8 MB heap ile bitti ve daha hızlıydı; toplam allocation neredeyse aynı, ama nesneler doğup hemen öldüğü için GC onları ucuza topluyor. Dosya 10 kat büyüse list yöntemi çöker, stream yöntemi aynı 8 MB ile çalışmaya devam eder.

Utf8JsonReader: Ne Zaman Gerekir

Tablodaki üçüncü satır en düşük seviyeli yol. Utf8JsonReader JSON'u token token okuyan bir ref struct; hiçbir nesne üretmez. Biz yalnız lineTotal değerlerini topladık:

public static decimal SumLineTotals(Stream source)
{
    byte[] buffer = ArrayPool<byte>.Shared.Rent(64 * 1024);
    try
    {
        int filled = source.Read(buffer);
        var state = new JsonReaderState();
        decimal sum = 0;
        bool expectLineTotal = false;

        while (true)
        {
            bool isFinal = filled < buffer.Length && source.Position == source.Length;
            var reader = new Utf8JsonReader(buffer.AsSpan(0, filled), isFinal, state);

            while (reader.Read())
            {
                if (reader.TokenType == JsonTokenType.PropertyName)
                {
                    expectLineTotal = reader.ValueTextEquals("lineTotal"u8);
                }
                else if (expectLineTotal && reader.TokenType == JsonTokenType.Number)
                {
                    sum += reader.GetDecimal();
                    expectLineTotal = false;
                }
            }

            if (isFinal) return sum;

            // Okunmamış kuyruğu başa al, arkasına yeni veri oku.
            state = reader.CurrentState;
            int consumed = (int)reader.BytesConsumed;
            int leftover = filled - consumed;
            buffer.AsSpan(consumed, leftover).CopyTo(buffer);
            if (leftover == buffer.Length)
            {
                throw new InvalidDataException("A single JSON token is larger than the buffer.");
            }
            filled = leftover + source.Read(buffer.AsSpan(leftover));
        }
    }
    finally
    {
        ArrayPool<byte>.Shared.Return(buffer);
    }
}

Üç kat daha hızlı ve allocation yok denecek kadar az. Ama kodun neredeyse tamamı buffer muhasebesi: isFinalBlock, JsonReaderState ile kaldığı yerden devam etme, buffer'ın sonunda yarım kalan token'ı başa kaydırma. Üçüncü yazıdaki Pipelines tartışmasının aynısı. Ayrıca lineTotal adını başka bir nesnede de görse toplardı; yapıyı bilmeyen bu kod, şema değiştiğinde sessizce yanlış sonuç üretebilir. Utf8JsonReader'ı, ölçülmüş bir darboğazda ve tek bir alanı çok büyük veriden çekmen gerektiğinde kullan; geri kalan her durumda DeserializeAsyncEnumerable yeterli. Yazma tarafındaki karşılığı Utf8JsonWriter; bir önceki benchmark'ta fast path'in hedefi olarak gördük.

JsonSchemaExporter ile şema üretmek

.NET 9 ile gelen JsonSchemaExporter, bir tipin options'a göre nasıl serialize edileceğini JSON Schema olarak verir. Muhasebe ekibine export'un formatını belgelemek ya da bir iş ortağına contract göndermek için kullanışlı:

JsonNode schema = JsonSchemaExporter.GetJsonSchemaAsNode(JsonSerializerOptions.Web, typeof(OrderItem));
{
  "type": [
    "object",
    "null"
  ],
  "properties": {
    "isbn": {
      "type": "string"
    },
    "title": {
      "type": "string"
    },
    "quantity": {
      "type": [
        "string",
        "integer"
      ],
      "pattern": "^-?(?:0|[1-9]\\d*)$"
    },
    "unitPrice": {
      "type": [
        "string",
        "number"
      ],
      "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$"
    },
    "lineTotal": {
      "type": [
        "string",
        "number"
      ],
      "pattern": "^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?$"
    }
  },
  "required": [
    "isbn",
    "title",
    "quantity",
    "unitPrice"
  ]
}

Şema options'ı birebir yansıtıyor: Web ayarları tırnak içindeki sayıları kabul ettiği için quantity hem "string" hem "integer" olabiliyor. Constructor parametreleri required listesine girmiş, hesaplanan lineTotal ise girmemiş. Şemanın "bizim API'miz bunu kabul eder" dediğini unutma; daha sıkı bir contract istiyorsan önce options'ı sıkılaştır.

Newtonsoft.Json'dan Geçiş

Eski bir projeyi STJ'ye taşırken en sık karşılaşılan farklar:

Newtonsoft.JsonSystem.Text.Json karşılığıNot
JsonConvert.SerializeObjectJsonSerializer.SerializeOptions instance'ını cache'le
[JsonProperty("name")][JsonPropertyName("name")]Attribute'lar farklı namespace'te
Harf duyarsız eşleşme (varsayılan)PropertyNameCaseInsensitive ya da JsonSerializerOptions.WebSTJ varsayılanı harf duyarlı
Yorum ve sondaki virgülü tolere ederReadCommentHandling.Skip, AllowTrailingCommasSTJ varsayılanı reddeder
ReferenceLoopHandling.IgnoreReferenceHandler.IgnoreCyclesPreserve ile $id/$ref da var
JObject, JArray, JTokenJsonNode, JsonObject, JsonArraySalt okuma için JsonDocument
TypeNameHandling[JsonPolymorphic] ve [JsonDerivedType]Yalnız izin verilen tipler
DefaultValueHandling.IgnoreJsonIgnoreCondition.WhenWritingDefaultNull için WhenWritingNull

Yorum ve sondaki virgül farkı, elle düzenlenen configuration dosyalarında ya da eski bir ERP'nin (Enterprise Resource Planning; muhasebe, stok ve satışı yöneten kurumsal yazılım) ürettiği dosyalarda hemen görünür:

Web: '/' is invalid after a value. Expected either ',', '}', or ']'. Path: $ | LineNumber: 1 | BytePositionInLine: 2.
Lenient: OrderItem { Isbn = 9780441172719, Title = Dune, Quantity = 2, UnitPrice = 349.90, LineTotal = 699.80 }

Geçişi tek seferde yapma. Önce mevcut endpoint'lerin gerçek isteklerinden ve yanıtlarından bir "golden" JSON seti topla; iki serializer'ın aynı nesneden aynı JSON'u ürettiğini ve aynı JSON'dan aynı nesneyi okuduğunu testlerle doğrula. Sonra modül modül geç: MVC (Model-View-Controller; ASP.NET Core'un controller tabanlı web modeli) controller'larında AddNewtonsoftJson ile eski davranış korunabilir, yeni endpoint'ler STJ ile başlar. En çok zaman alan kısım genelde custom JsonConverter'lar ve JObject ile yazılmış dinamik kodlar; onları listeleyip ayrı ayrı planla.

Üretimde Dikkat

Büyük veri için hiçbir zaman List<T>'ye deserialize etme. Export ve import'larda DeserializeAsyncEnumerable ile yazma tarafında IAsyncEnumerable<T> kullan. Tablodaki fark 1 milyon siparişte 100 katı aştı; container'ın bellek limiti 512 MB ise list yöntemi zaten tamamlanmazdı.

Kullanıcıdan gelen JSON'da boyut ve derinlik sınırı koy. Kestrel'in request body limiti (varsayılan yaklaşık 30 MB) ilk savunma hattı; STJ'nin MaxDepth ayarı (varsayılan 64) aşırı iç içe JSON'a karşı koruma sağlar.

Source generation'ı derleme kontrolü gibi kullan. Context'e eklenmeyen bir tip AOT'ta çalışma zamanında hata verir; bunu yakalamanın en ucuz yolu, CI'da (Continuous Integration; her commit'te otomatik build ve test) trimming uyarılarını hata olarak derleyen bir adım.

Ölçümü kendi verinle yap. Bu yazıdaki sayılar bir geliştirici makinesinden; mutlak değerler senin donanımında farklı olur, ama oranlar (836 MB'a karşı 8 MB, fast path'te sıfır allocation) benzer kalır.

Sık Düşülen Tuzaklar

JsonDocument'ı dispose etmemek. Pool'dan kiralanan buffer geri dönmez; yük altında bellek kullanımı yavaş yavaş artar. Her zaman using.

JsonElement'i document'tan uzun yaşatmak. Bir cache'e ya da başka bir task'a verilen element, document kapandığında ObjectDisposedException fırlatır. Gerekirse Clone().

Context'e sarmalayıcı tipleri eklemeyi unutmak. IAsyncEnumerable<Order> ya da List<Order> gibi sarmalayıcı tipler ayrı ayrı [JsonSerializable] ister; eksikse reflection'a düşülür ya da AOT'ta hata alınır.

Yalnız Serialization modunu seçmek. Fast path'in kullanılamadığı ilk çağrıda düşülecek metadata olmadığı için exception gelir. Varsayılan modda kal ya da hangi çağrıların fast path'e girdiğini ölç.

Utf8JsonReader'ı her şeye çözüm sanmak. Hızlıdır, ama buffer yönetimi ve yapı bilgisi senin sorumluluğunda. Ölçülmüş bir darboğaz yoksa tipli streaming deserialization daha güvenli.

Küçük Bir Deneme

★☆☆ summary komutuna bir JsonNode modu ekle: dosyayı JsonNode.Parse ile okuyup ciroyu hesapla. Peak heap'i list ve stream ile karşılaştır.

★★☆ BookstoreJsonContext'ten UseStringEnumConverter'ı kaldırıp yerine OrderStatus'a [JsonConverter(typeof(JsonStringEnumConverter<OrderStatus>))] koy. Benchmark'taki fast path satırının bu değişiklikten nasıl etkilendiğini ölç.

★★★ SumLineTotals'ı yalnız items array'i içindeki lineTotal'ları sayacak şekilde değiştir: reader.CurrentDepth ile hangi seviyede olduğunu takip et. Test için siparişin köküne "lineTotal": 1000000 diye sahte bir alan ekle ve eski kodun yanlış sonucunu gör.

JSON bitti; bir sonraki yazıda XML'e geçiyoruz. XmlReader ile streaming okuma, LINQ (Language Integrated Query; C#'ın dile gömülü sorgu özelliği) tabanlı LINQ to XML, XmlSerializer ve DataContractSerializer arasındaki fark, namespace'ler ve XSD (XML Schema Definition; XML belgesinin yapısını tanımlayan şema dili) doğrulama, son olarak da XXE'ye (XML External Entity; dış entity tanımlarıyla sunucuya dosya okutma saldırısı) karşı güvenli varsayılanlar: hepsini UBL (Universal Business Language; e-faturada kullanılan XML standardı) benzeri bir fatura belgesi üzerinden göreceğiz.