CSV ile Çalışmak: RFC 4180, Türkçe Excel, Encoding ve CSV Injection
CSV'yi string.Split ile değil, RFC 4180'in tırnak kurallarını bilen bir okuyucuyla ve her sayıyı açık bir culture ile parse etmek gerekir; tr-TR sunucuda decimal.Parse("1234.50") sessizce 123450 döner. Windows-1254 kodlu bir banka dosyasını CsvHelper 34 ile hatalı satırda durmadan import ediyor, Türkçe Excel'de doğru açılan export'u, CSV injection korumasını ve CsvHelper, Sep, Sylvan ölçümünü gösteriyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET 10, CsvHelper 34.0, Sep 0.18, Sylvan.Data.Csv 1.4 ve BenchmarkDotNet 0.15.8 ile derlenip denenmiştir.
Önce Tahmin Et
Bookstore'un (sitedeki serilerin ortak örnek uygulaması, bir online kitapçı) tahsilat import'u bankadan gelen tutarları şöyle okuyor:
decimal amount = decimal.Parse("1234.50");
Sunucunun culture'ı tr-TR. amount kaç olur? Exception mı alırsın, 1234.50 mi, başka bir şey mi?
İkinci soru: aşağıdaki dosyanın ikinci veri satırını line.Split(',') ile bölersen kaç alan çıkar?
OrderNumber,CustomerName,Note,Total
BS-0000001,Ada,Gift wrap,349.90
BS-0000002,"Lovelace, Ada","Leave at the door, ring twice",189.50
Üçüncüsü: bir müşteri adını =1+1 olarak girmiş. Muhasebeye giden sipariş export'unu CsvHelper'ın varsayılan ayarlarıyla yazarsan bu hücre dosyaya nasıl düşer?
XML'den sonra en az onun kadar yaygın ama çok daha az kurala sahip bir formata geçiyoruz: CSV (Comma-Separated Values; değerleri ayırıcıyla sıralayan düz metin formatı). Örnekler serinin repo'sunda Bookstore.IO.Csv console uygulamasında ve benchmark projesinde:
dotnet run --project src/Bookstore.IO.Csv -- split
dotnet run -c Release --project benchmarks/Bookstore.IO.Benchmarks -- --filter "*CsvBenchmarks*"
RFC 4180 ve string.Split'in Bozulduğu Yer
CSV'nin herkesin atıf yaptığı tanımı RFC (Request for Comments; internet standartlarının yayımlandığı belge serisi) 4180, 2005 tarihli ve statüsü "Informational": bir standart koymuyor, yaygın kullanımı belgeliyor. Söyledikleri kısaca şunlar: her kayıt CRLF (satır sonunu belirten carriage return ve line feed karakter çifti) ile biten bir satırdır, son satırdaki satır sonu isteğe bağlıdır, ilk satır isteğe bağlı bir header olabilir, her kayıtta aynı sayıda alan olmalıdır. Ayırıcı, çift tırnak ya da satır sonu içeren alan çift tırnak içine alınır; tırnaklı alanın içindeki çift tırnak iki kez yazılarak kaçırılır (""). Boşluklar alanın parçasıdır, kırpılmaz.
Söylemedikleri daha önemli. Ayırıcı yalnız virgüldür; noktalı virgül, tab ya da | RFC'de yoktur. Grameri yalnız yazdırılabilir ASCII (İngilizce harfler, rakamlar ve temel işaretlerden oluşan 128 karakterlik set) karakterlere izin verir; encoding'i MIME (dosyanın içerik türünü bildiren standart etiket) tipinin charset parametresine bırakır ve dosyanın içinde encoding bilgisi taşımanın bir yolu yoktur. Sayı, tarih, null, boolean gibi tiplerden hiç söz etmez: 1.234,50 bir sayı mı, metin mi, RFC'ye göre bu bir soru bile değil. 2014'teki RFC 7111 yalnız text/csv için URI (Uniform Resource Identifier; bir kaynağı tanımlayan adres) fragment'leri ekledi; bu boşlukları doldurmadı. Sonuç: gerçek hayattaki CSV dosyaları RFC 4180'in tanımına kabaca uyar, ama her üretici kendi lehçesini konuşur.
İkinci sorunun cevabı. Örnek dosyanın üçüncü siparişinde notun içinde bir satır sonu da var. Önce herkesin ilk yazdığı kod:
foreach (string line in NotesCsv.Split("\r\n", StringSplitOptions.RemoveEmptyEntries).Skip(1))
{
string[] fields = line.Split(',');
Console.WriteLine($" {fields.Length} fields, Total = {fields[^1]}");
}
string.Split:
4 fields, Total = 349.90
6 fields, Total = 189.50
3 fields, Total = "Deliver to ""Building B""
2 fields, Total = 259.00
İkinci satır altı alana bölündü, çünkü Split tırnağın içindeki virgülü bilmiyor. Üçüncü sipariş ise iki "satıra" ayrıldı ve tutar sütununda bir not parçası okundu. Burada son sütunu okuduğumuz için tutar tesadüfen doğru göründü; fields[3] yazsaydın ikinci siparişin tutarı " ring twice" olurdu. Bu tür hatalar testte görünmez, çünkü test verisinde virgüllü bir müşteri adı yoktur.
RFC 4180'i doğru okumak için satır bazlı değil, karakter bazlı bir state machine gerekir: tırnağın içinde miyiz, değil miyiz. Asgari hâli şu:
public static List<string>? ReadRecord(TextReader reader, char separator = ',')
{
int c = reader.Read();
if (c == -1) return null;
var fields = new List<string>();
var field = new StringBuilder();
bool inQuotes = false;
while (true)
{
if (inQuotes)
{
if (c == -1) throw new FormatException("Unclosed quote at end of input.");
if (c == '"')
{
// "" tırnağın içinde tek bir çift tırnaktır; tek " alanı kapatır.
if (reader.Peek() == '"') { field.Append('"'); reader.Read(); }
else inQuotes = false;
}
else field.Append((char)c);
}
else if (c == '"' && field.Length == 0) inQuotes = true;
else if (c == separator) { fields.Add(field.ToString()); field.Clear(); }
else if (c is '\r' or '\n' or -1)
{
if (c == '\r' && reader.Peek() == '\n') reader.Read();
fields.Add(field.ToString());
return fields;
}
else field.Append((char)c);
c = reader.Read();
}
}
RFC 4180:
4 fields, Total = 349.90, Note = Gift wrap
4 fields, Total = 189.50, Note = Leave at the door, ring twice
4 fields, Total = 259.00, Note = Deliver to "Building B"\n2nd floor
Üç sipariş, her biri dört alan. Bu kod ne zaman yeterli? Dosyayı senin sistemin üretiyorsa, birkaç bin satırsa ve bir NuGet bağımlılığı eklemek istemiyorsan. Ama gerçek bir import'ta bunun üstüne header eşleştirme, tip dönüşümü, hatalı tırnak toleransı, satır numaralı hata mesajları, encoding ve performans gelir. O noktada bir library kullanmak daha ucuzdur.
Library Seçimi: CsvHelper, Sep ve Sylvan
.NET'te üç ciddi seçenek var. Lisanslarını ve sürümlerini yazım günü NuGet paketlerinin kendisinden kontrol ettik:
| Library | Sürüm (yayın) | Lisans | Hedef | Öne çıkan |
|---|---|---|---|---|
| CsvHelper | 34.0.0 (27 Ekim 2026) | Microsoft Public License (MS-PL) veya Apache-2.0 | .NET Framework 4.7, netstandard2.0, net8.0, net10.0 | Source generator ile tip mapping, zengin hata yönetimi, injection koruması |
| Sep | 0.18.0 (11 Kasım 2026) | MIT (kısıtlaması çok az olan açık kaynak lisansı) | net8.0 ve üstü | Span tabanlı, SIMD (tek komutla birden çok veriyi işleyen CPU komutları), en düşük allocation; sütun bazlı API |
| Sylvan.Data.Csv | 1.4.4 (7 Nisan 2026) | MIT | netstandard2.0, net6.0 | DbDataReader olarak çalışır, SqlBulkCopy gibi veritabanı API'lerine doğrudan verilebilir |
CsvHelper için önemli bir not: 34.0 bir yeniden yazım. Eski CsvConfiguration yerine immutable CsvReaderOptions/CsvWriterOptions record'ları geldi, fluent ClassMap yerine [CsvRecord] ile işaretlenen partial tipler için compile-time'da kod üreten bir source generator var, constructor'daki culture parametresi kalktı ve varsayılan culture artık invariant. İnternetteki örneklerin çoğu 33 ve öncesine ait; projede 33 kullanıyorsan geçişi resmi migration rehberiyle planla. Bu yazıdaki kodlar 34 ile.
Performans iddialarını kendimiz ölçtük: invariant formatta, 100.000 siparişlik (6,3 MB) bir CSV'yi bellekten okuyup tutar sütununu topladık. StringSplit, yukarıda bozulduğunu gördüğümüz yöntem; burada veri tırnaksız olduğu için doğru sonuç veriyor ve karşılaştırma tabanı olarak duruyor.
[Benchmark]
public decimal CsvHelperRecords()
{
using var csv = CsvHelper.CsvReader.Create(new MemoryStream(_data), CsvHelper.CsvReaderOptions.Default);
decimal total = 0;
foreach (OrderSummaryRow row in csv.GetRecords<OrderSummaryRow>()) total += row.Total;
return total;
}
[Benchmark]
public decimal Sep()
{
using var reader = nietras.SeparatedValues.Sep.Reader().From(new MemoryStream(_data));
int index = reader.Header.IndexOf("Total");
decimal total = 0;
foreach (var row in reader) total += row[index].Parse<decimal>();
return total;
}
| Method | Orders | Mean | Error | StdDev | Ratio | Gen0 | Gen1 | Allocated | Alloc Ratio |
|----------------- |------- |---------:|----------:|----------:|------:|----------:|--------:|------------:|------------:|
| StringSplit | 100000 | 8.867 ms | 0.1773 ms | 0.5058 ms | 1.00 | 5390.6250 | 15.6250 | 44071.22 KB | 1.000 |
| CsvHelperRecords | 100000 | 5.444 ms | 0.0546 ms | 0.0426 ms | 0.62 | 2210.9375 | 7.8125 | 18073.3 KB | 0.410 |
| CsvHelperFields | 100000 | 1.835 ms | 0.0073 ms | 0.0057 ms | 0.21 | - | - | 1.14 KB | 0.000 |
| Sep | 100000 | 4.305 ms | 0.0115 ms | 0.0107 ms | 0.49 | - | - | 4 KB | 0.000 |
| Sylvan | 100000 | 7.958 ms | 0.1589 ms | 0.4268 ms | 0.90 | - | - | 37.65 KB | 0.001 |
Bu ölçümde üç library de Split'ten hızlı ve tutarı topladığımız sürece üçü de neredeyse hiç allocation yapmıyor; Split ise her satır için bir satır string'i, bir array ve beş alan string'i ürettiği için 44 MB allocation yaptı. En hızlısı CsvHelper'ın alan bazlı API'si oldu (csv.Read() ve csv.Parse<decimal>(4)): 34 sürümü dosyayı UTF (Unicode Transformation Format; Unicode karakterlerini byte'a çeviren kodlama ailesi) ailesinden UTF-8 byte'ları olarak okuyor ve sayıları string üretmeden doğrudan bu byte'lardan parse ediyor. Aynı library ile her satırı bir OrderSummaryRow object'ine çevirmek üç kat yavaş ve 18 MB allocation demek; fark library'den değil, 100.000 object ve string'den geliyor. Sep, Sylvan'ın yaklaşık iki katı hızlı. Bu sayıları bir genelleme olarak okuma: Sep'in kendi benchmark'ları başka dosya şekilleri ve başka işlemlerle farklı bir sıralama gösterebilir. Mutlak sayılar bir geliştirici makinesinden. Pratik bir kural: tip mapping, hata raporlama ve injection koruması gereken iş dosyalarında CsvHelper; milyonlarca satırı birkaç sütun için tarayan, allocation'a duyarlı kodda Sep ya da CsvHelper'ın alan API'si; veriyi doğrudan SqlBulkCopy ile SQL Server'a basacaksan Sylvan'ın DbDataReader olması işini kolaylaştırır.
Türkiye Gerçeği: Noktalı Virgül, Ondalık Virgül ve Culture
Türkçe bölge ayarlarında ondalık ayırıcı virgüldür. Virgül sayının içinde kullanıldığı için Windows'un liste ayırıcısı da noktalı virgüldür ve Excel, çift tıklanarak açılan bir CSV'yi bu liste ayırıcısına göre böler. Bu yüzden Türkiye'de bankadan, ERP'den (Enterprise Resource Planning; muhasebe, stok ve satın almayı yöneten kurumsal yazılım) ya da muhasebecinin Excel'inden gelen "CSV" dosyalarının çoğu aslında noktalı virgülle ayrılmıştır ve sayıları 1.234,50 biçimindedir. .NET'te aynı bilgiler CultureInfo'da:
ListSeparator tr-TR: ';', invariant: ','
ToString("N2") tr-TR: 1.234,50, invariant: 1,234.50
decimal.Parse("1234.50", tr-TR) = 123450
decimal.Parse("1.234,50", tr-TR) = 1234.50
decimal.Parse("1.234,50", invariant): FormatException: The input string '1.234,50' was not in a correct format.
DateTime.Parse("03.11.2026", tr-TR) = 2026-11-03
DateTime.Parse("03.11.2026", invariant) = 2026-03-11
İlk sorunun cevabı üçüncü satırda: exception yok, sonuç 123450. Türkçe culture'da nokta binlik ayırıcıdır ve decimal.Parse binlik ayırıcının yerini kontrol etmez. 1.234,50 TL'lik bir tahsilat sessizce yüz kat büyür. Tarih tarafı da aynı derecede sinsi: 03.11.2026 Türkçe culture'da 3 Kasım, invariant culture'da 11 Mart. Ay 12'den büyük olmadığı sürece hiçbir hata almazsın.
Kural basit ve istisnasız: dosyadan okunan her sayı ve tarih açık bir IFormatProvider ile parse edilir. Dosya Türkçe biçimdeyse CultureInfo.GetCultureInfo("tr-TR"), makineler arası bir formatsa CultureInfo.InvariantCulture. Sunucunun culture'ına güvenmek, kodun geliştirici makinesinde ve sunucuda farklı sonuç vermesi demektir. Bu serinin console uygulamaları CultureInfo.CurrentCulture'ı açılışta invariant'a çekiyor; tarihlerde mümkünse ParseExact ile tam formatı ver.
Windows-1254 ile Gelen Banka Dosyası
Bookstore'un bankası günlük tahsilat dosyasını eski bir sistemden üretiyor: noktalı virgülle ayrılmış, Türkçe header'lı, Türkçe sayı ve tarih biçimli ve Windows-1254 (Microsoft'un Türkçe için tanımladığı tek byte'lık kod sayfası) ile kodlanmış. İlk satırları UTF-8 sanarak okursak:
GetEncoding(1254) without provider: No data is available for encoding 1254. For information on defining a custom encoding, see the documentation for the Encoding.RegisterProvider method.
UTF-8 (default): ��lem Tarihi;A��klama;Tutar;Referans
Windows-1254: İşlem Tarihi;Açıklama;Tutar;Referans
02.11.2026;BS-0000001 sipariş ödemesi - LİNUS;1.116,40;TRX00000001
İki ayrı sorun görüyoruz. Birincisi, File.ReadLines encoding verilmezse UTF-8 varsayar; 1254'teki İ, ş, ç, ı byte'ları geçerli UTF-8 olmadığı için yerine Unicode'un "çözülemeyen karakter" işareti � (U+FFFD) konur. Exception yok, veri sessizce bozulur ve geri döndürülemez. İkincisi, .NET (Core) yalnız UTF ailesini, ASCII ve Latin-1'i içinde taşır; 1254 gibi kod sayfaları için CodePagesEncodingProvider'ı bir kez kaydetmek gerekir:
// Uygulama açılışında bir kez.
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Encoding windows1254 = Encoding.GetEncoding(1254);
Provider .NET'in kendisinde var, ayrı bir paket gerekmez. Banka satırındaki LİNUS da ayrı bir not: banka adı Türkçe kurallarla büyük harfe çevirmiş, noktalı İ ile. Bu adı müşteri kaydıyla ToUpperInvariant kullanarak karşılaştırırsan LINUS ile eşleşmez.
Import: hatalı satırda durmamak
Dosyanın şekli tipte duruyor. CsvHelper 34'te [CsvRecord] ile işaretlenen partial class için mapping kodunu source generator üretir; reflection yoktur:
[CsvRecord]
public sealed partial class BankPayment
{
[CsvColumn("İşlem Tarihi"), CsvFormat("dd.MM.yyyy")]
public DateOnly Date { get; init; }
[CsvColumn("Açıklama")]
public string Description { get; init; } = "";
[CsvColumn("Tutar")]
public decimal Amount { get; init; }
[CsvColumn("Referans")]
public string Reference { get; init; } = "";
}
Bin satırlık bir dosyada üç satır bozuksa ne yapmalı? Tüm import'u durdurmak, 997 doğru tahsilatın da işlenmemesi demek; muhasebe ekibi sabah dosyayı elle düzeltip tekrar gönderene kadar siparişler "ödeme bekliyor"da kalır. Daha iyi strateji: doğru satırları işle, hatalıları satır numarası ve ham içeriğiyle raporla. CsvHelper'da bunun yeri OnRecordError:
var errors = new List<string>();
var options = new CsvReaderOptions
{
Separator = ';',
Encoding = Encoding.GetEncoding(1254),
FormatProvider = Turkish,
TrackLineNumbers = true,
// Hatalı satırı raporla ve atla; import durmasın.
OnRecordError = error =>
{
long line = (error.Exception as CsvFormatException)?.LineNumber ?? 0;
errors.Add($"line {line}: {error.Exception.Message} | {error.RawRecord}");
return true;
}
};
using var csv = CsvReader.FromFile(path, options);
foreach (BankPayment payment in csv.GetRecords<BankPayment>())
{
imported++;
total += payment.Amount;
}
Imported 997 payments, total 872,536.90 TRY, 3 errors
line 401: Cannot read BankPayment.Amount from field 2 'Tutar' of record 401 (line 401): The input string '1.2O4,50' was not in a correct format. | 02.11.2026;BS-0000400 sipariş ödemesi - LİNUS;1.2O4,50;TRX00000400
line 601: Cannot read BankPayment.Date from field 0 'İşlem Tarihi' of record 601 (line 601): String '31.11.2026' was not recognized as a valid DateOnly. | 31.11.2026;BS-0000600 sipariş ödemesi - LİNUS;988,40;TRX00000600
line 801: Cannot read BankPayment.Amount from field 2 'Tutar' of record 801 (line 801): The input string '' was not in a correct format. | 02.11.2026;BS-0000800 sipariş ödemesi - ADA
Üç hata, üçü de işe yarar bir mesajla: sıfır yerine büyük O harfi yazılmış bir tutar, takvimde olmayan bir tarih (30 gün çeken Kasım'ın 31'i) ve son iki alanı eksik bir satır. 250. satırdaki açıklama ise tırnak içinde bir noktalı virgül taşıyordu ve sorunsuz okundu. Encoding verildiği için CsvHelper 1254'ü okurken UTF-8'e çeviriyor, FormatProvider sayesinde 1.116,40 doğru parse ediliyor. OnRecordError'dan true dönmek kaydı atlamak demek; 33 ve öncesindeki ReadingExceptionOccurred'da anlam tersti, eski kodu taşırken dikkat et.
Hata raporunun gerçek bir sistemde nereye gideceği ayrı bir karar: bir "reddedilen satırlar" dosyası, muhasebeye e-posta ya da bir tablo. Önemli olan import'un sonunda "997 işlendi, 3 reddedildi" bilgisinin kaybolmaması ve reddedilen satırların düzeltilip tekrar gönderilebilmesi.
Muhasebe İçin Excel'de Doğru Açılan Export
Ters yön: muhasebe ekibi sipariş export'unu Excel'de çift tıklayarak açıyor. CsvHelper'ın varsayılan ayarlarıyla yazılmış dosya:
First bytes: 53697061
Sipariş No,Müşteri,Tarih,Durum,Tutar
BS-0000001,Linus,01.11.2026 00:00:07,Pending,1116.40
BS-0000002,=1+1,01.11.2026 00:00:14,Pending,559.30
BS-0000003,Linus,01.11.2026 00:00:21,Pending,996.30
BS-0000004,@Grace,01.11.2026 00:00:28,Shipped,1096.00
BS-0000005,Ada,01.11.2026 00:00:35,Pending,1758.20
RFC 4180'e uygun bir dosya, ama Türkçe bölge ayarlı bir Excel için üç sorunu var. Ayırıcı virgül olduğu için Excel her satırı tek bir hücreye koyar. Tutarlar noktalı olduğu için sayı olarak tanınmaz. Dosyanın başında BOM (Byte Order Mark; dosyanın başında kodlamayı belirten byte'lar) olmadığı için Excel UTF-8'i tanımaz ve dosyayı Windows'un ANSI (Windows'ta sistem diline göre seçilen varsayılan tek byte'lık kod sayfası) kod sayfasıyla okur; header'daki ş ve ü bozuk görünür. Üç ayarla düzeliyor:
// Excel'in Türkçe bölge ayarlarıyla doğru açacağı biçim.
public static readonly CsvWriterOptions ExcelTurkish = new()
{
Separator = ';',
FormatProvider = CultureInfo.GetCultureInfo("tr-TR"),
WriteByteOrderMark = true,
Injection = CsvInjection.Escape
};
First bytes: EFBBBF53
Sipariş No;Müşteri;Tarih;Durum;Tutar
BS-0000001;Linus;01.11.2026 00:00:07;Pending;1116,40
BS-0000002;"'=1+1";01.11.2026 00:00:14;Pending;559,30
BS-0000003;Linus;01.11.2026 00:00:21;Pending;996,30
BS-0000004;"'@Grace";01.11.2026 00:00:28;Shipped;1096,00
BS-0000005;Ada;01.11.2026 00:00:35;Pending;1758,20
EF BB BF, UTF-8 BOM'u. Tutarlar artık ondalık virgüllü ve binlik ayırıcısız; binlik ayırıcı koymamak, dosya başka bir araçla okunduğunda karışıklığı azaltır. Bu dosyanın bir bedeli var: artık yalnız Türkçe ayarlı Excel için doğru. Aynı export'u bir başka sisteme, bir veri ambarına ya da yurtdışındaki bir iş ortağına gönderiyorsan invariant culture ve virgülle ayrı bir "makine formatı" üret. İki tüketiciye tek dosya yetmez.
Bazı sistemler dosyanın ilk satırına sep=; yazarak Excel'e ayırıcıyı bildirir. Excel'de işe yarar, ama diğer CSV okuyucuları için bu satır veridir; CsvHelper'ın okuyucusu bu satırı DetectSeparatorHint ile tanıyabilir. Kontrol sende değilse BOM ve doğru ayırıcı daha az sürpriz çıkarır.
CSV Injection: Hücreyi Formül Olmaktan Çıkarmak
Üçüncü sorunun cevabı ilk export'ta: CsvHelper 34'ün varsayılanı CsvInjection.None, yani =1+1 dosyaya olduğu gibi yazılır. Excel ya da başka bir spreadsheet bu hücreyi açarken formül olarak değerlendirir. Müşteri adı, kitap yorumu, adres notu gibi kullanıcının yazdığı her metin alanı bu yolla muhasebecinin bilgisayarında bir formüle dönüşebilir. Saldırı türünün ayrıntıları OWASP'ın (Open Web Application Security Project; web güvenliği üzerine çalışan açık topluluk) CSV Injection sayfasında; burada yalnız savunma tarafına bakıyoruz.
OWASP'ın önerisi, hücrenin başındaki =, +, -, @, tab, CR (carriage return; satır başı karakteri) ve LF (line feed; yeni satır karakteri) karakterlerini etkisizleştirmek: alanı çift tırnak içine almak, başına tek tırnak koymak ve içerideki çift tırnakları kaçırmak. Excel tek tırnakla başlayan hücreyi metin olarak gösterir. CsvInjection.Escape ikinci çıktıda tam olarak bunu yaptı: "'=1+1". Ayar yalnız metin alanlarına uygulanıyor, bu yüzden -149,90 gibi negatif bir iade tutarı sayı olarak kalır.
Üç sınırlamayı bilmek gerekiyor. Birincisi, CsvHelper'ın varsayılan karakter listesi = @ + -, tab ve CR; OWASP'ın listesindeki LF ve tam genişlikli Unicode varyantları (=, + gibi) bu listede yok ve liste yalnız ASCII karakter kabul ediyor. Bu varyantları da kapsamak istiyorsan export'tan önce kendi kontrolünü ekle. İkincisi, Excel dosyayı açıp tekrar CSV olarak kaydettiğinde kaçış karakterleri kaybolabilir ve formül yeniden etkin hâle gelebilir. Üçüncüsü, tek tırnak veride kalır: dosyayı bir program okuyacaksa o tırnağı temizlemesi gerekir. Bu yüzden injection korumasını yalnız insan tarafından spreadsheet'te açılacak export'larda aç; makine formatındaki dosyaya veri olduğu gibi gitsin. Injection türlerinin genel anlatımı ve girdi/çıktı kodlama ilkeleri OWASP Top 10 serisinin A05: Injection yazılarında.
Üretimde Dikkat
Lehçeyi belgeleyip kodla sabitle. "Banka dosyası" bir sözleşmedir: ayırıcı, encoding, sayı ve tarih biçimi, header adları. Hepsini CsvReaderOptions ve [CsvRecord] tipinde açıkça yaz; CsvHelper'ın ayırıcıyı otomatik tespit etmesi keşif için iyidir, üretimde tahmin yerine sözleşmeyi kullan.
Boyut ve satır sınırı koy. Kapanmamış bir tırnak, dosyanın geri kalanını tek bir alana dönüştürmeye çalışır. CsvHelper 34'ün MaxRecordSize ayarı tam olarak buna karşı; kullanıcıdan gelen dosyalarda bir üst sınır belirle ve toplam dosya boyutunu upload aşamasında kontrol et.
Import'u idempotent yap. Banka aynı dosyayı iki kez gönderebilir ya da muhasebe düzeltilmiş dosyayı tekrar yükleyebilir. Her satırın Referans değerini benzersiz anahtar olarak kullan ve daha önce işlenmiş referansları atla.
Library'yi ölçerek seç. Saniyede birkaç dosya işleyen bir import'ta CsvHelper ile Sep arasındaki fark ihmal edilebilir; gigabyte'larca veriyi her gece işleyen bir job'da ise önemlidir. Büyük dosyaları sabit bellekle işlemeyi serinin ilerideki büyük dosyalar yazısında ele alacağız.
Sık Düşülen Tuzaklar
string.Split(',') ile parse etmek. Virgüllü bir müşteri adı ya da çok satırlı bir not gelene kadar çalışır. Kendi dosyan bile olsa en azından tırnağı bilen bir okuyucu kullan.
Sayı ve tarihleri culture'sız parse etmek. decimal.Parse("1234.50") Türkçe culture'da exception vermeden 123450 döner. Her zaman açık bir IFormatProvider.
Encoding'i varsaymak. UTF-8 olmayan bir dosyayı UTF-8 diye okumak veriyi geri döndürülemez şekilde bozar. Dosyanın encoding'i sözleşmenin parçası; bilmiyorsan örnek bir dosyanın byte'larına bak, � görmeden önce.
Hatalı satırda bütün import'u durdurmak. Bir satırlık bir hata yüzlerce doğru kaydı bekletmemeli. Atla, raporla, devam et; ama hata oranı belli bir eşiği geçerse (dosyanın formatı değişmiş olabilir) import'u durdurmak da doğru karardır.
Sep'te Unescape'i unutmak. Sep performans için varsayılan olarak tırnakları çözmez; tırnaklı bir alan değerini tırnaklarıyla birlikte döner. Tırnak içeren dosyalarda SepReaderOptions.Unescape = true ver.
Küçük Bir Deneme
★☆☆ Rfc4180.ReadRecord'a "ab"c gibi kapanış tırnağından sonra metin içeren bir alan ver. Ne dönüyor? CsvHelper aynı girdiyi MalformedData = CsvMalformedDataHandling.Throw ile nasıl karşılıyor?
★★☆ Banka import'una bir eşik ekle: hatalı satırların oranı yüzde 5'i geçerse import'u durdur ve hiçbir satırı işleme. İpucu: dosyayı iki kez okumak yerine geçerli kayıtları önce bir listede biriktir.
★★★ Export'a OWASP listesindeki tam genişlikli karakterleri (=, +, -, @) de kapsayan kendi koruma adımını ekle ve CsvInjection.Escape ile birlikte bir test yaz. Negatif tutarların sayı olarak kaldığını doğrula.
CSV'yi bitirdik. Bir sonraki yazıda bu dosyaların çoğunun gerçek hayattaki taşıyıcısına geçiyoruz: compression ve arşivler. GZip, Deflate, ZLib ve Brotli arasındaki farka, ZIP (dosyaları sıkıştırıp tek arşivde toplayan format) arşivleri için .NET 10'un async API'lerine, "tüm faturalar" zip'ini diske dosya yazmadan response'a streaming göndermeye ve yüklenen bir zip'i Zip Slip'e karşı güvenle açmaya bakacağız.