# DbDataReader ile Akış Tabanlı JSON ve XML Serileştirme

> DbDataReader sonuçlarını DataTable, satır başına sözlük veya DTO grafiği oluşturmadan; şema önbelleği, türe özgü okuma, Utf8JsonWriter ve XmlWriter ile doğrudan akışa yazan C# yaklaşımı.

- Author: Muhammet Ali Köker
- Language: tr
- Canonical: https://alikoker.com.tr/dbdatareader-akiskan-json-xml-serilestirme
- Translation: https://alikoker.com.tr/en/streaming-json-xml-serialization-from-dbdatareader
- Published: 2020-07-03T15:27:00+03:00
- Modified: 2026-08-26T23:04:00+03:00
- Verified: 2026-08-26T23:04:00+03:00
- Type: article

Bu çalışma, eski C# yardımcı sınıflarımdaki küçük bir performans kararından doğdu. Veritabanı sonucunu JSON ya da XML'e aktarırken sorun çoğu zaman yazıcının kendisi değildi; `DbDataReader` ile çıktı akışı arasına eklenen `DataTable`, DTO listesi veya satır başına sözlük gibi ara temsillerdi. Sorgu zaten satırları ileri yönlü veriyorsa, aynı sonucu ikinci kez bellekte kurmanın bedelini ayrıca ödemek gerekmiyordu.

Özellikle büyük sonuç kümelerinde bu bedel yalnız toplam bellek değildir. Her satır için yeni nesne üretimi, sözlük anahtarlarının dolaşılması, kolon adlarının yeniden çözülmesi ve sonunda oluşan nesne grafiğinin bir serileştirici tarafından tekrar ziyaret edilmesi sık çalışan yürütme yolunu uzatır. `DataTable`, `DataSet`, `List<Dictionary<string, object>>` ve benzeri yapılar bazı işlerde gereklidir; burada ise hedef yalnız çalışan bir sorgunun sonucunu dışarı aktarmaktır.

Eski kodumda `DbRow`, okuyucunun o anda işaret ettiği satırı `IDictionary<string, object>` benzeri bir görünümle açıyordu. Kolon adlarını başlangıçta topluyor, `yield return` sırasında her kayıt için ayrı sözlük kurmak yerine aynı hafif satır görünümünü kullanıyordum. `IEnumerable` tabanlı CSV çalışmamda da aynı düşünce vardı: **değişmeyen üst veriyi bir kez hazırla; tekrar edilen döngüde yalnız gerçekten satıra özgü işi bırak.**

Yayın için hazırladığım sürüm bu fikri iki farklı kullanım yoluna ayırıyor:

1. `DbReaderEnumerable`, okuyucunun mevcut satırını kopyalamadan yeniden kullanılan bir `DbRowView` ile dolaştırıyor.
2. `DbReaderSerializer`, yalnız JSON/XML dışa aktarımı isteniyorsa `IEnumerable` görünümüne bile uğramıyor; `Read() -> türe özgü okuma -> yazıcı` zincirini doğrudan işletiyor.

Bu ayrım bilinçli. Burada ORM, yansıma (`reflection`) tabanlı eşleyici veya genel amaçlı bir serileştirme çerçevesi kurmaya çalışmıyorum. Sözleşme daha dar: **hazır bir `DbDataReader` al, mevcut ileri yönlü akışı bozma ve sonucu mümkün olduğunca az ara durumla hedef `Stream`'e yaz.**

## DataReader zaten doğru başlangıç noktası

ADO.NET `DataReader` için önemli özellik, sonuçların tamamını önce bellekte oluşturma zorunluluğu olmamasıdır. Okuma ileri yönlüdür; sonuçlar sorgu yürüdükçe tüketilebilir. Microsoft belgelerindeki “ileri yönlü / salt okunur” (`forward-only / read-only`) sözleşmesini burada doğrudan mimari sınır olarak kullanıyorum.

Büyük bir sorguyu önce şu zincire çevirmek:

```text
DbDataReader
    -> DataTable
    -> List<Row>
    -> JSON/XML serileştirici
```

çoğu dışa aktarma senaryosunda gereksiz bir ara model oluşturur.

Benim tercih ettiğim yol şöyledir:

```text
DbDataReader
    -> şema önbelleği
    -> geçerli satır
    -> JSON/XML yazıcısı
```

İkinci zincirde amaç, satır sayısı arttıkça yardımcı veri yapılarının da aynı oranda büyümemesidir. Yazıcının kendi tamponu, sağlayıcının ağ tamponu ve o anda okunan satır elbette bellektedir; fakat uygulama tarafında sonuç kümesinin tamamını temsil eden ikinci bir koleksiyon kurulmaz.

## Tarihsel Db `IEnumerable` fikri

İlk sürümde `DbRow`, okuyucunun kolon adlarını kurucuda alıyordu. Numaralandırıcı (`Enumerator`) her satırda yeni bir `Dictionary` üretmek yerine aynı `DbRow` nesnesini döndürüyordu. Böylece satır değerleri ancak serileştirici veya çağıran kod istediğinde doğrudan okuyucudan alınıyordu.

Tarihsel gerçeklenimin zayıf yanı, iyi olan fikrin sağlayıcı ayrıntısına gömülmüş olmasıydı. `DbRow` doğrudan `OleDbDataReader` taşıdığı halde ODBC, MySQL ve Oracle için yazılmış `IEnumerable` sınıfları da aynı satır tipine yaslanıyordu; bu, ortak bir soyutlama görüntüsü verse de tip sözleşmesini gerçekten ortaklaştırmıyordu. Üstelik `IDictionary` seçimi, ileri yönlü bir satır görünümünde anlamı olmayan ekleme/silme/değiştirme üyelerini de API'ye taşıyor ve bunlar `NotSupportedException` ile kapatılıyordu.

Yayın sürümünde aynı davranışı daha açık bir sözleşmeyle kurdum:

- sağlayıcıya özgü okuyucu yerine ortak `DbDataReader` kullanılıyor;
- satır görünümü `IReadOnlyDictionary<string, object?>` sözleşmesine sahip;
- `IEnumerable` katmanı açıkça **tek geçişli**;
- aynı `DbRowView` nesnesi satırlar arasında yeniden kullanılıyor;
- kolon adı -> sıra numarası (`ordinal`) sözlüğü yalnız bir kez kuruluyor;
- yinelenen kolon adları sessizce ezilmek yerine reddediliyor ve SQL takma adı kullanılması isteniyor.

Çekirdeğin sağlayıcı paketlerinden bağımsız kalması burada teorik bir soyutlama hedefi değil, doğrudan kullanım sınırıdır. SQL Server, Oracle, MySQL, PostgreSQL, ODBC veya başka bir sürücü çağıran tarafta kalır; sonuç `DbDataReader` tabanından geliyorsa aynı `IEnumerable` ve serileştirici yolu kullanılabilir.

## Şemayı bir kez çıkarmak

Bir sonuç kümesinde okuyucu açıldıktan sonra kolon sıra numaraları, adlar ve sağlayıcının bildirdiği CLR tipleri satırdan satıra yeniden tanımlanmaz. Kodda bu özelliği varsayım olarak bırakmak yerine açıkça bir `DbReaderSchema` planına dönüştürdüm.

Başlangıçta her kolon için aşağıdaki üst veri hazırlanır:

```text
sıra numarası (ordinal)
kolon adı
JSON için önceden kodlanmış özellik adı
XML'e uygun öğe adı
CLR alan tipi
değer yazma kategorisi
```

JSON yolunda özellik adları daha ilk aşamada `JsonEncodedText` olarak hazırlanır. Böylece aynı kolon adı her kayıt için yeniden UTF-8 kodlama/kaçış işleminden geçirilmez; `Utf8JsonWriter` için bilinen özellik adlarının önceden kodlanması Microsoft'un performans önerisiyle de uyumludur.

XML tarafında aynı kolon adı `XmlConvert.EncodeLocalName` ile yalnız bir kez XML'e uygun biçime çevrilir. Boşluk, noktalama veya XML ad kurallarına uymayan karakterlerin maliyeti böylece satır döngüsüne taşınmaz.

Şema oluşturulurken bir başka karar daha verilir: kolon adı -> sıra numarası (`ordinal`) tablosu `StringComparer.OrdinalIgnoreCase` ile bir kez kurulur. Aynı sonuç kümesinde yinelenen kolon adı çıkarsa sonuncuyu sessizce ezmek yerine işlem `InvalidOperationException` ile durdurulur. Bu davranış özellikle birleştirme (`JOIN`) sonuçlarında önemlidir; çözüm serileştiricinin rastgele seçim yapması değil, SQL tarafında açık bir takma ad vermektir.

Şema hazırlama maliyeti yaklaşık olarak:

```text
O(C)
```

seviyesindedir. Burada `C` kolon sayısıdır.

Sonraki sık çalışan yol:

```text
while (reader.Read())
    her kolon için
        null?
        türe özgü okuma
        yazıcı
```

şeklindedir.

## JSON: ara nesne olmadan UTF-8 çıktısı

`Utf8JsonWriter`, .NET'in ileri yönlü ve önbellek oluşturmayan UTF-8 JSON yazıcısıdır. Genel `JsonSerializer` da kendi alt katmanında bu yazıcıyı kullanır; fakat veritabanı sonucu gibi şeması zaten belli bir veri kaynağında nesne grafiği kurmaya gerek yoktur.

Güncel akış doğrudan şu biçimdedir:

```csharp
using DbDataReader reader = command.ExecuteReader(CommandBehavior.SequentialAccess);
DbReaderSerializer.WriteJson(reader, outputStream);
```

Çıktı standart bir JSON dizisidir:

```json
[
  {"id":1,"name":"..."},
  {"id":2,"name":"..."}
]
```

Bu yolda satır başına `Dictionary<string, object>` kurulmaz, DTO özellikleri yansıma (`reflection`) ile keşfedilmez ve tamamı bellekte duran büyük bir JSON `string` oluşturulup sonradan UTF-8'e çevrilmez. `Utf8JsonWriter` doğrudan hedef `Stream` üzerinde ilerler; metodun dönüş değeri de yazılan satır sayısıdır. `leaveReaderOpen` varsayılan olarak `true` olduğu için okuyucunun yaşam döngüsü çağıran kodun kontrolünde kalır; istenirse serileştirme tamamlandığında belirli bir sırayla kapatılabilir.

### Tiplerin korunması

Veritabanı değerini her durumda `ToString()` ile yazmak kolaydır ancak JSON anlamını bozar. Sayılar, mantıksal değerler ve `null` metne dönüşür.

Bu nedenle şema hazırlanırken yaygın CLR tipleri sınıflandırılır:

- `string`
- `bool`
- işaretli/işaretsiz tamsayı tipleri
- `float`, `double`, `decimal`
- `DateTime`, `DateTimeOffset`
- `DateOnly`, `TimeOnly`, `TimeSpan`
- `Guid`
- `byte[]`

Sıcak yolda `Type` karşılaştırması yerine önceden hesaplanmış küçük bir enum üzerinden türe özgü okuma yöntemi seçilir. Sayılar JSON sayısı, mantıksal değerler JSON boolean, `DBNull` ise `null` olarak yazılır. `byte[]` Base64 temsil edilir.

Sağlayıcıya özgü bilinmeyen bir CLR tipiyle karşılaşılırsa yalnız o alan kültürden bağımsız bir metin geri dönüş yoluna gider. Bu davranış, bütün kolonları en baştan string'e çevirmekten daha sınırlı bir uyumluluk katmanıdır.

## XML: DOM kurmadan ileri yönlü yazma

XML için de aynı prensip geçerlidir. `XmlDocument` veya başka bir DOM ağacı kurmak yerine `XmlWriter` kullanılır. Microsoft `XmlWriter`'ı hızlı, önbellek oluşturmayan ve ileri yönlü XML üretme API'si olarak tanımlar.

```csharp
using DbDataReader reader = command.ExecuteReader(CommandBehavior.SequentialAccess);
DbReaderSerializer.WriteXml(reader, outputStream);
```

Üretilen yapı kavramsal olarak şöyledir:

```xml
<rows xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <row>
    <id>1</id>
    <name>...</name>
    <optional_note xsi:nil="true" />
  </row>
</rows>
```

Burada `NULL` ile boş metin aynı şey değildir. Bu nedenle veritabanı `NULL` değeri XML'de `xsi:nil="true"` ile ayrıştırılır.

## `IEnumerable` ne zaman gerekli?

Doğrudan JSON/XML dışa aktarımında en kısa yol `DbReaderSerializer`'dır. Buna rağmen `IEnumerable` katmanı yararlıdır:

```csharp
using var rows = reader.AsRows();
foreach (DbRowView row in rows)
{
    Console.WriteLine(row["id"]);
}
```

Bu yapı özellikle:

- satır bazlı filtreleme,
- istatistik çıkarma,
- özel format üretimi,
- mevcut `IEnumerable` tabanlı kodla entegrasyon

için kullanışlıdır.

Buradaki en kolay yanlış kullanım `DbRowView`'ı kalıcı kayıt sanmaktır. Değildir. Numaralandırıcı ilerlediğinde aynı görünüm nesnesi okuyucunun yeni geçerli satırına bakar. Birkaç `DbRowView` referansını listeye koyup daha sonra okumak, farklı anlık görüntüler biriktirmek anlamına gelmez.

Bu yüzden geçici görünüm davranışı API sözleşmesinin görünür parçasıdır. Kalıcı veri gerekiyorsa anlık görüntü tam ihtiyaç duyulan sınırda bilinçli biçimde alınmalıdır. Nesne tahsisini azaltmayı “ücretsiz optimizasyon” gibi sunmak yerine karşılığındaki kullanım kuralını açık tutmayı tercih ettim.

## Neden genel amaçlı serileştirici yerine özel yazıcı?

Genel amaçlı bir serileştirici, aldığı nesnenin yapısını çalışma zamanında çözmek zorundadır. Sözlükte anahtar/değer çiftleri dolaşılır; DTO'da özellik üst verisi çözümlenir; farklı dönüştürücüler ve adlandırma ilkeleri devreye girebilir.

Veritabanı okuyucusunda ise elimizde zaten şunlar vardır:

```text
kolon sıra numarası (`ordinal`)
kolon adı
kolon CLR tipi
mevcut değer
```

Bu veri kaynağında serileştiricinin ihtiyaç duyduğu şemanın önemli bir bölümü zaten okuyucuda mevcut. Ek bir nesne grafiği kurmak yerine mevcut üst veriyi başlangıçta küçük bir yazma planına dönüştürmek, burada daha kısa ve daha öngörülebilir bir yürütme yolu veriyor.

`IEnumerable` tabanlı CSV çalışmamla bağ da tam bu noktada. CSV'de başlık ile model özelliklerini bir kez eşleyip satır döngüsünü konumsal hale getiriyordum. Bu çalışmada model katmanı da ortadan kalkıyor; okuyucu şeması doğrudan JSON/XML yazma planına dönüşüyor.

## Karmaşıklık ve bellek davranışı

`N` satır, `C` kolon olduğunu kabul edelim.

Şema hazırlama:

```text
O(C)
```

Satırların serileştirilmesi:

```text
O(N * C)
```

JSON/XML çıktısının kendisini yazmak zaten en az bu kadar alanı ziyaret etmeyi gerektirir. Asıl amaç asimptotik karmaşıklığı değiştirmek değil, `N * C` sık çalışan yolundaki gereksiz işlemleri azaltmaktır.

Yardımcı bellek yaklaşık olarak:

```text
O(C + yazıcı tamponu + geçerli okuyucu satırı)
```

şeklindedir.

Şunlar sonuç kümesinin tamamı kadar büyümez:

- `DataTable`
- `List<T>`
- satır başına sözlük koleksiyonu
- DOM
- tam JSON/XML metni

Elbette gerçek bellek tüketiminde ADO.NET sağlayıcısının ağ tamponu, sürücü davranışı ve büyük LOB kolonları da etkilidir.

## Büyük BLOB/CLOB sınırı

Bu gerçeklenim temel olarak tablo biçimli skaler sonuçlar ve normal büyüklükte `byte[]` alanlar için tasarlanmıştır. `byte[]` alan Base64 yazılırken sağlayıcıdan bütün değer alınır.

Çok büyük BLOB/CLOB alanları için ayrı bir akış üzerinden kopyalama yolu daha doğru olabilir:

```text
DbDataReader.GetStream / GetTextReader
    -> sınırlı tampon
    -> hedef yazıcı
```

Bu optimizasyonu genel çekirdeğe otomatik eklemedim. Birkaç kilobaytlık ikili alan ile birkaç gigabaytlık LOB aynı kullanım sözleşmesine sahip değildir.

## Veritabanı güvenliği başka bir katmandır

İlk tarihsel sınıflar bağlantı dizesi ve SQL metnini de taşıyordu. Yayın sürümünde bu sorumluluk özellikle çıkarıldı. Kütüphane **hazır bir `DbDataReader` alır**.

Bunun iki yararı vardır:

1. sağlayıcı paketleri ve bağlantı yaşam döngüsü çekirdeğe bağlanmaz;
2. SQL'in nasıl oluşturulduğu serileştiricinin sorumluluğu haline gelmez.

Parametreli sorgu, en az yetki, kimlik bilgisi saklama ve işlem yönetimi politikası yine çağıran uygulamanın sorumluluğundadır. Hızlı serileştirici, güvenli sorgu oluşturmanın alternatifi değildir.

## Kıyaslama ölçümünde neden sayı vermiyorum?

Bu mimarinin hangi maliyetleri ortadan kaldırdığı kod üzerinden açıkça gösterilebilir. Ancak "X kat hızlı" demek için:

- aynı ADO.NET sağlayıcısı,
- aynı sorgu,
- aynı ağ koşulu,
- aynı kolon tipleri,
- aynı veri büyüklüğü,
- aynı JSON/XML formatı,
- aynı GC ve çalışma zamanı sürümü

ile tekrarlanabilir bir kıyaslama ölçümü gerekir.

Bu nedenle makaleye “X kat hızlı” gibi kaynağı olmayan bir oran koymadım. Koddan kesin olarak görülebilen şey; sonuç kümesinin tamamını ikinci kez bellekte oluşturmadığı, satır başına sözlük oluşturmadığı ve değişmeyen üst veriyi sıcak döngüden çıkardığıdır. Gerçek aktarım hızı karşılaştırması eklenecekse aynı sağlayıcı, sorgu, veri dağılımı, çalışma zamanı ve GC ayarlarıyla ayrıca ölçülmelidir.

## Önceki CSV Enumerable çalışmasıyla ilişki

Bu yapı, daha önce yayımladığım **Enumerable CSV İşleme Algoritması** ile aynı performans düşüncesini farklı veri kaynağına uygular.

CSV tarafında:

```text
özellik üst verisi -> bir kez
başlık eşleme      -> bir kez
satırlar          -> konumsal sık çalışan yol
```

Bu çalışmada:

```text
okuyucu şeması         -> bir kez
JSON/XML alan planı    -> bir kez
satırlar               -> türe özgü yazma yolu
```

İki yaklaşımın ortak noktası karmaşık bir çerçeve kurmak değil, değişmeyen bilgiyi tekrar hesaplamamaktır.

## Sonuç

Bu gerçeklenimde performans kararının merkezi serileştirici seçimi değil, veri akışının biçimidir. Okuyucudan çıkan satırı önce başka bir koleksiyon modeline dönüştürmediğimde hem yaşam süresi hem bellek davranışı daha kolay gerekçelendirilebilir.

`DbDataReader` ileri yönlü tüketilir; `Utf8JsonWriter` ve `XmlWriter` da ileri yönlü üretir. Aradaki katmanın görevi yalnız şemayı bir kez hazırlamak ve her alanı doğru yazma yoluna göndermektir:

```text
DB -> okuyucu -> yazıcı -> akış
```

Benim açımdan asıl kazanç yalnız nesne tahsisi sayısını düşürmek değil. Sonuç kümesi büyüdüğünde uygulama tarafındaki yardımcı belleğin hangi yapılar nedeniyle büyüyebileceği açık kalıyor; okuyucunun yaşam döngüsü çağıranın elinde tutuluyor; yinelenen kolon adı gibi belirsiz durumlar sessizce kabul edilmiyor ve farklı ADO.NET sağlayıcıları aynı serileştirme çekirdeğine bağlanabiliyor. Performans iyileştirmesi, davranışın daha belirgin hale gelmesiyle birlikte geliyor.

## Kaynakça

1. Microsoft. *DataAdapters and DataReaders - ADO.NET*. https://learn.microsoft.com/en-us/dotnet/framework/data/adonet/dataadapters-and-datareaders
2. Microsoft. *How to use Utf8JsonWriter in System.Text.Json*. https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/use-utf8jsonwriter
3. Microsoft. *XmlWriter Class*. https://learn.microsoft.com/en-us/dotnet/api/system.xml.xmlwriter
4. Muhammet Ali Köker. *Enumerable CSV İşleme Algoritması*. https://alikoker.com.tr/enumerable-csv-isleme-algoritmasi
5. Muhammet Ali Köker. *db-reader-streaming-serialization-csharp*. GitHub. [db-reader-streaming-serialization-csharp](https://github.com/alikoker/db-reader-streaming-serialization-csharp)

## Açık Kaynak Kod

Makalede incelenen güncel gerçeklenim açık kaynak olarak GitHub üzerinde yayımlanmıştır:

**Kaynak kod:** [db-reader-streaming-serialization-csharp](https://github.com/alikoker/db-reader-streaming-serialization-csharp)

Depo, `DbReaderEnumerable`, `DbReaderSerializer`, şema planı, türe özgü yazma yolları, örnek kullanım ve testleri içerir. Makale teknik gerekçeyi ve tasarım sınırlarını açıklarken GitHub deposu çalıştırılabilir gerçeklenimi taşır. Kaynak arşivinde makale kopyaları tutulmaz; depo üst verisi İngilizce kanonik makaleye `streaming-json-xml-serialization-from-dbdatareader` slug'ı üzerinden bağlanır.

## Yazılım Kaydı

**GitHub:** [db-reader-streaming-serialization-csharp](https://github.com/alikoker/db-reader-streaming-serialization-csharp)

**Kalıcı yazılım arşivi (Zenodo DOI):** [10.5281/zenodo.22117281](https://doi.org/10.5281/zenodo.22117281)

## Bu Çalışmaya Atıf

Köker, M. A. (2020). DbDataReader ile Akış Tabanlı JSON ve XML Serileştirme. alikoker.com.tr. https://alikoker.com.tr/dbdatareader-akiskan-json-xml-serilestirme

- BibTeX: https://alikoker.com.tr/dbdatareader-akiskan-json-xml-serilestirme.bib
- RIS: https://alikoker.com.tr/dbdatareader-akiskan-json-xml-serilestirme.ris
- CSL-JSON: https://alikoker.com.tr/dbdatareader-akiskan-json-xml-serilestirme.csl.json
