XML ile Çalışmak: XmlReader, LINQ to XML, XmlSerializer ve Güvenli Varsayılanlar
Büyük XML'i XmlReader ile gezip her belgeyi kısa ömürlü bir XElement'e çeviren hibrit okuma, LINQ to XML'in rahatlığını sabit bellekle birleştirir: 267 MB'lık dosyada XDocument 703 MB, hibrit yöntem 8,5 MB heap kullandı. UBL benzeri bir Bookstore faturasıyla namespace'leri, XSD doğrulamayı, XmlSerializer ile DataContractSerializer farkını, XmlSerializer'ın cache'lenmeyen constructor'larını ve XXE'ye karşı .NET'in varsayılanlarını gösteriyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir.
Önce Tahmin Et
Bookstore (sitedeki serilerin ortak örnek uygulaması, bir online kitapçı) faturalarını UBL (Universal Business Language; e-faturada kullanılan XML standardı) benzeri bir XML olarak üretiyor. Muhasebe ekibinden biri fatura satırlarını saymak için şunu yazıyor:
int lines = invoice.Root!.Elements("InvoiceLine").Count();
Faturada üç satır var. Bu kod kaç döner?
İkinci soru: kargo firmasına giden XML'in kök element adı farklı olmalı, bu yüzden her istekte new XmlSerializer(typeof(ShipmentNotice), new XmlRootAttribute("Notice")) çağrılıyor. 200 istekten sonra process'te kaç tane yeni assembly vardır?
Üçüncüsü: içinde <!DOCTYPE> tanımı olan bir XML'i XmlReader.Create, XDocument.Parse ve XmlDocument.LoadXml ile okursan üçü de aynı şekilde mi davranır?
JSON'u iki yazıda ele aldık; XML hâlâ e-fatura, banka entegrasyonları, kargo ve eski SOAP (Simple Object Access Protocol; XML tabanlı eski web service protokolü) service'leriyle gündelik işin parçası. Örnekler serinin repo'sunda Bookstore.IO.Xml console uygulamasında:
dotnet run --project src/Bookstore.IO.Xml -- read
Hangi API Ne Zaman
.NET'te XML ile çalışmanın beş ana yolu var ve her biri farklı bir ihtiyaca göre tasarlanmış:
| API | Model | Ne zaman |
|---|---|---|
XmlReader / XmlWriter | İleri yönlü, streaming, async destekli | Büyük dosyalar, sabit bellek, en düşük allocation |
XDocument / XElement (LINQ to XML) | Bellekte değiştirilebilir ağaç | Tek bir belgeyi üretmek, okumak, sorgulamak |
XmlDocument | Eski DOM (Document Object Model; belgeyi bellekte ağaç olarak tutan model), XPath ağırlıklı | Mevcut kodun bakımı; yeni kodda XDocument |
XmlSerializer | Public property'lerden XML, attribute'larla tam kontrol | Formatı dışarıdan dayatılan entegrasyonlar |
DataContractSerializer | Opt-in contract, yalnız element | WCF (Windows Communication Foundation; .NET Framework'ün eski service altyapısı) dünyasından gelen contract'lar |
Pratikte en sık kullanılan ikili XmlReader ve LINQ to XML. Birazdan ikisini birlikte kullanan bir hibrit yöntem göreceğiz.
LINQ to XML: UBL Benzeri Fatura ve Namespace'ler
Türkiye'de e-fatura, GİB'in (Gelir İdaresi Başkanlığı) UBL 2.1 üzerine kurduğu UBL-TR formatını kullanır. Gerçek bir entegrasyonda şemayı ve sürümünü GİB'in güncel paketinden almak gerekir; bu yazıda UBL 2.1'in gerçek namespace'lerini kullanan ama element seti sadeleştirilmiş, kendi yazdığımız bir şemayla çalışıyoruz. Amaç e-fatura kurallarını öğretmek değil, namespace yoğun bir belgeyle .NET'te nasıl çalışıldığını göstermek.
UBL'de element'ler üç namespace'e dağılır: belgenin kökü (Invoice-2), birleşik bileşenler (cac) ve temel bileşenler (cbc). LINQ to XML'de namespace bir XNamespace nesnesidir ve element adı namespace + "LocalName" ile kurulur:
public static class Ubl
{
public static readonly XNamespace Invoice = "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2";
public static readonly XNamespace Cac = "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2";
public static readonly XNamespace Cbc = "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2";
}
public static XDocument Create(Order order, DateOnly issueDate)
{
XNamespace cac = Ubl.Cac, cbc = Ubl.Cbc;
return new XDocument(
new XDeclaration("1.0", "utf-8", null),
new XElement(Ubl.Invoice + "Invoice",
new XAttribute(XNamespace.Xmlns + "cac", cac),
new XAttribute(XNamespace.Xmlns + "cbc", cbc),
new XElement(cbc + "UBLVersionID", "2.1"),
new XElement(cbc + "ID", order.OrderNumber.Replace("BS-", "BSI2026")),
new XElement(cbc + "IssueDate", issueDate.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)),
new XElement(cbc + "DocumentCurrencyCode", order.Currency),
new XElement(cac + "AccountingCustomerParty",
new XElement(cac + "Party",
new XElement(cac + "PartyName",
new XElement(cbc + "Name", order.CustomerName)))),
order.Items.Select((item, index) =>
new XElement(cac + "InvoiceLine",
new XElement(cbc + "ID", index + 1),
new XElement(cbc + "InvoicedQuantity", new XAttribute("unitCode", "C62"), item.Quantity),
new XElement(cbc + "LineExtensionAmount", new XAttribute("currencyID", order.Currency), Amount(item.LineTotal)),
new XElement(cac + "Item",
new XElement(cbc + "Name", item.Title),
new XElement(cac + "StandardItemIdentification",
new XElement(cbc + "ID", item.Isbn))),
new XElement(cac + "Price",
new XElement(cbc + "PriceAmount", new XAttribute("currencyID", order.Currency), Amount(item.UnitPrice))))),
new XElement(cac + "LegalMonetaryTotal",
new XElement(cbc + "PayableAmount", new XAttribute("currencyID", order.Currency), Amount(order.Total)))));
}
<?xml version="1.0" encoding="utf-8"?>
<Invoice xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2" xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<cbc:UBLVersionID>2.1</cbc:UBLVersionID>
<cbc:ID>BSI20260000003</cbc:ID>
<cbc:IssueDate>2026-11-14</cbc:IssueDate>
<cbc:DocumentCurrencyCode>TRY</cbc:DocumentCurrencyCode>
<cac:AccountingCustomerParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Linus</cbc:Name>
</cac:PartyName>
</cac:Party>
</cac:AccountingCustomerParty>
<cac:InvoiceLine>
<cbc:ID>1</cbc:ID>
<cbc:InvoicedQuantity unitCode="C62">2</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="TRY">299.80</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>Frankenstein</cbc:Name>
<cac:StandardItemIdentification>
<cbc:ID>9780141439471</cbc:ID>
</cac:StandardItemIdentification>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="TRY">149.90</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
...
<cac:LegalMonetaryTotal>
<cbc:PayableAmount currencyID="TRY">996.30</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
</Invoice>
Kodun şekli belgenin şekliyle aynı; LINQ to XML'in en güçlü yanı bu. xmlns:cac ve xmlns:cbc attribute'larını biz eklemesek de belge doğru olurdu, ama .NET prefix'leri kendisi üretirdi (p1, p2 gibi). Karşı tarafın beklediği prefix'leri açıkça tanımlamak hem okunabilirlik hem de prefix'e bakan kötü yazılmış parser'lar için iyi bir alışkanlık. C62, Birleşmiş Milletler Avrupa Ekonomik Komisyonu'nun (UN/ECE) birim kodlarında "adet" anlamına geliyor. Tutarlar yine InvariantCulture ile yazılıyor; XML'de ondalık ayırıcı her zaman nokta.
Şimdi ilk soru. Namespace'li bir belgede element'i yalnız yerel adıyla aramak hiçbir şey bulmaz:
Console.WriteLine($"Without namespace: {invoice.Root!.Elements("InvoiceLine").Count()} lines");
Console.WriteLine($"With namespace: {invoice.Root!.Elements(Ubl.Cac + "InvoiceLine").Count()} lines");
Without namespace: 0 lines
With namespace: 3 lines
9780141439471 Frankenstein x2 = 299.80 TRY
9780141439587 Emma x1 = 189.50 TRY
9780141439686 Persuasion x3 = 507.00 TRY
"InvoiceLine" string'i XName'e çevrilirken namespace'siz bir ad olur ve cac:InvoiceLine ile eşleşmez. Exception yok, yalnız boş bir sonuç; testte boş bir listeyle geçen ve üretimde faturaların sıfır satırlı görünmesine yol açan sinsi bir hata. Satırları okuyan kod, element'lere explicit cast ile değer veriyor:
return invoice.Root!
.Elements(cac + "InvoiceLine")
.Select(line => new InvoiceLineInfo(
Isbn: (string)line.Element(cac + "Item")!.Element(cac + "StandardItemIdentification")!.Element(cbc + "ID")!,
Title: (string)line.Element(cac + "Item")!.Element(cbc + "Name")!,
Quantity: (decimal)line.Element(cbc + "InvoicedQuantity")!,
LineTotal: (decimal)line.Element(cbc + "LineExtensionAmount")!,
Currency: (string)line.Element(cbc + "LineExtensionAmount")!.Attribute("currencyID")!));
(decimal)element dönüşümü XML Schema kurallarına göre ve culture'dan bağımsız parse eder; decimal.Parse(element.Value) yazmaktan daha güvenlidir. Attribute'ların genelde namespace'i olmaz; currencyID'yi yalın adla arıyoruz.
XSD ile Doğrulama
Dışarıdan gelen bir faturayı işlemeden önce şemaya uyduğunu kontrol etmek, hatalı belgeyi iş mantığının derinlerinde değil kapıda yakalar. Üç namespace için üç XSD (XML Schema Definition; XML belgesinin yapısını tanımlayan şema dili) dosyamız var; hepsini bir XmlSchemaSet'e ekleyip okurken doğruluyoruz:
public static List<string> Validate(TextReader xml, XmlSchemaSet schemas)
{
var errors = new List<string>();
var settings = new XmlReaderSettings
{
ValidationType = ValidationType.Schema,
Schemas = schemas,
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null
};
settings.ValidationEventHandler += (_, e) =>
errors.Add($"{e.Severity} line {e.Exception.LineNumber}: {e.Message}");
using var reader = XmlReader.Create(xml, settings);
while (reader.Read()) { }
return errors;
}
Valid invoice: 0 errors
Missing ID: 1 errors
Error line 3: The element 'Invoice' in namespace 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2' has invalid child element 'IssueDate' in namespace 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2'. List of possible elements expected: 'ID' in namespace 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2'.
Bad values: 2 errors
Error line 15: The 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2:InvoicedQuantity' element is invalid - The value 'two' is invalid according to its datatype 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2:QuantityType' - The string 'two' is not a valid Decimal value.
Error line 56: The required attribute 'currencyID' is missing.
ValidationEventHandler verdiğimiz için reader ilk hatada durmuyor, hepsini topluyoruz; handler vermeseydin ilk hatada XmlSchemaValidationException alırdın. Satır numaraları, iş ortağına "faturanızın 15. satırında miktar sayı değil" diye net bir geri bildirim vermeni sağlar.
Bir ayrıntıya dikkat: ilk denemede belgeyi bilerek üç yerden bozmuştuk, ama yalnız bir hata gelmişti. Sıralama hatası (ID beklenirken IssueDate gelmesi) ortaya çıktığında validator o element'in geri kalan içeriğini güvenilir şekilde doğrulayamıyor ve sonraki hataları raporlamıyor. Bu yüzden ikiye böldük. Sıfır hata "belge tamamen doğru" demek, bir hata ise "en az bir hata var" demek.
Büyük XML: XmlReader ile Element Element Okumak
Ay sonunda bütün faturalar tek bir dosyada toplanıyor: 200.000 fatura, 267 MB. Bunu yazmak için XmlWriter'ı async modda açıyoruz; her fatura küçük bir XElement olarak üretilip hemen dosyaya yazılıyor:
var settings = new XmlWriterSettings { Async = true, Indent = false, Encoding = new System.Text.UTF8Encoding(false) };
await using XmlWriter writer = XmlWriter.Create(destination, settings);
await writer.WriteStartDocumentAsync();
await writer.WriteStartElementAsync(null, "InvoiceBatch", "urn:bookstore:invoices:batch");
await writer.WriteAttributeStringAsync("xmlns", "cac", null, Ubl.Cac.NamespaceName);
await writer.WriteAttributeStringAsync("xmlns", "cbc", null, Ubl.Cbc.NamespaceName);
foreach (var order in orders)
{
// Her fatura küçük bir XElement olarak üretilir ve hemen yazılır.
XElement invoice = InvoiceXml.Create(order, issueDate).Root!;
invoice.Attributes().Where(a => a.IsNamespaceDeclaration && a.Name.Namespace == XNamespace.Xmlns).Remove(); // cac/cbc batch kökünde bir kez tanımlı
await invoice.WriteToAsync(writer, CancellationToken.None);
}
await writer.WriteEndElementAsync();
await writer.WriteEndDocumentAsync();
cac ve cbc prefix'lerini batch'in kökünde bir kez tanımlayıp her faturadaki tekrarlarını sildik; ilk denemede bu tekrar dosyaya 33 MB eklemişti. Okuma tarafında iki yolu karşılaştırdık. Biri dosyanın tamamını XDocument.Load ile almak; diğeri hibrit yöntem: XmlReader dosyada ilerliyor, her Invoice element'ine gelince yalnız o element'i XNode.ReadFromAsync ile kısa ömürlü bir XElement'e çeviriyor:
var settings = new XmlReaderSettings { Async = true, DtdProcessing = DtdProcessing.Prohibit, XmlResolver = null };
using XmlReader reader = XmlReader.Create(source, settings);
XName payable = Ubl.Cbc + "PayableAmount";
int count = 0;
decimal total = 0;
await reader.MoveToContentAsync();
while (!reader.EOF)
{
if (reader.NodeType == XmlNodeType.Element && reader.LocalName == "Invoice" && reader.NamespaceURI == Ubl.Invoice.NamespaceName)
{
// ReadFrom reader'ı element'in sonrasına taşır; burada ayrıca Read() çağrılmaz.
var invoice = (XElement)await XNode.ReadFromAsync(reader, CancellationToken.None);
total += (decimal)invoice.Descendants(payable).Single();
count++;
}
else
{
await reader.ReadAsync();
}
}
write 200,000 invoices in 1,996 ms, peak heap 4,136,688 bytes
stream 200,000 invoices, 177,944,049.50 TRY, file 267,026,162 bytes, 2,200 ms, peak heap 8,564,056 bytes
xdocument 200,000 invoices, 177,944,049.50 TRY, file 267,026,162 bytes, 1,928 ms, peak heap 703,209,472 bytes
Süreler yakın, bellek kullanımı ise 80 kat farklı: XDocument 267 MB'lık dosya için 703 MB heap kullandı, hibrit yöntem 8,5 MB. Her fatura içinde LINQ to XML'in rahatlığını korurken dosyanın tamamını hiçbir zaman belleğe almıyoruz. Döngüdeki küçük ama önemli ayrıntı yorumda: ReadFrom reader'ı element'in sonrasına taşıdığı için o dalda ayrıca Read çağırırsan bir sonraki faturayı atlarsın. JSON tarafında DeserializeAsyncEnumerable ile gördüğümüz sabit bellek prensibinin XML'deki karşılığı bu.
XmlSerializer ve DataContractSerializer
Kargo firması gönderim bildirimini belirli bir XML biçiminde istiyor: sipariş numarası attribute olarak, paketler bir Parcels listesinde, ağırlık attribute, ISBN (kitaplara verilen uluslararası standart numara) element'in metni. Aynı class'ı iki serializer için de işaretledik:
[XmlRoot("ShipmentNotice", Namespace = "urn:bookstore:shipping")]
[DataContract(Name = "ShipmentNotice", Namespace = "urn:bookstore:shipping")]
public sealed class ShipmentNotice
{
[XmlAttribute("orderNumber")]
[DataMember(Order = 1)]
public string OrderNumber { get; set; } = "";
[XmlElement("Carrier")]
[DataMember(Order = 2)]
public string Carrier { get; set; } = "";
[XmlElement("TrackingCode")]
[DataMember(Order = 3)]
public string TrackingCode { get; set; } = "";
[XmlArray("Parcels"), XmlArrayItem("Parcel")]
[DataMember(Order = 4)]
public List<Parcel> Parcels { get; set; } = [];
[XmlIgnore]
public decimal InternalCost { get; set; }
}
[DataContract(Namespace = "urn:bookstore:shipping")]
public sealed class Parcel
{
[XmlAttribute("weightKg")]
[DataMember(Order = 1)]
public decimal WeightKg { get; set; }
[XmlText]
[DataMember(Order = 2)]
public string Isbn { get; set; } = "";
}
XmlSerializer:
<ShipmentNotice xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" orderNumber="BS-0000042" xmlns="urn:bookstore:shipping">
<Carrier>ACME Cargo</Carrier>
<TrackingCode>TRK-123456</TrackingCode>
<Parcels>
<Parcel weightKg="0.8">9780441172719</Parcel>
<Parcel weightKg="0.5">9780141439587</Parcel>
</Parcels>
</ShipmentNotice>
DataContractSerializer:
<ShipmentNotice xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="urn:bookstore:shipping">
<OrderNumber>BS-0000042</OrderNumber>
<Carrier>ACME Cargo</Carrier>
<TrackingCode>TRK-123456</TrackingCode>
<Parcels>
<Parcel>
<WeightKg>0.8</WeightKg>
<Isbn>9780441172719</Isbn>
</Parcel>
...
</Parcels>
</ShipmentNotice>
Fark net. XmlSerializer XML'in şeklini tam olarak kontrol etmeni sağlar: attribute, element, metin, liste sarmalayıcısı, namespace. Karşılığında public bir parametresiz constructor ve public, set edilebilir property'ler ister. DataContractSerializer attribute üretmez, her şey element'tir; neyin serialize edileceğini [DataMember] ile açıkça seçersin (InternalCost işaretlenmediği için zaten dışarıda kaldı) ve Order verilmezse element'leri alfabetik sıralar. Formatı karşı taraf dayatıyorsa XmlSerializer; iki uç da senin kodunsa ve WCF'ten kalma bir contract yoksa çoğu zaman XML yerine JSON daha iyi bir seçim. XmlSerializer'ın her çıktıya eklediği xsi ve xsd tanımlarını istemiyorsan Serialize'a boş bir XmlSerializerNamespaces verebilirsin.
Cache'lenmeyen constructor'lar
İkinci sorunun cevabı. XmlSerializer ilk kullanımda, tip için serialization kodunu çalışma zamanında üretip dinamik bir assembly'ye derler. XmlSerializer(Type) ve XmlSerializer(Type, string) constructor'ları bu assembly'yi cache'ler; diğer overload'lar cache'lemez ve her çağrıda yenisini üretir:
for (int i = 0; i < 200; i++)
{
new XmlSerializer(typeof(ShipmentNotice)).Serialize(TextWriter.Null, notice);
}
var root = new XmlRootAttribute("Notice") { Namespace = "urn:bookstore:shipping" };
for (int i = 0; i < 200; i++)
{
new XmlSerializer(typeof(ShipmentNotice), root).Serialize(TextWriter.Null, notice);
}
Assemblies at start: 10
After 200 x XmlSerializer(Type): 24
After 200 x XmlSerializer(Type, XmlRootAttribute): 224 (191 ms)
Dynamic assemblies: 201
İlk döngü yalnız bir kez kod üretti (diğer artışlar System.Xml'in kendi assembly'leri). İkinci döngü her istekte yeni bir dinamik assembly üretti: 200 istekte 200 assembly. Bu assembly'ler boşaltılamaz; gün boyunca çalışan bir serviste bellek sürekli büyür ve her istek de yaklaşık 1 ms fazladan derleme maliyeti öder. Çözüm basit: bu overload'larla oluşturduğun serializer'ı bir kez yaratıp static bir field'da ya da tip ve root adına göre anahtarlı bir ConcurrentDictionary'de sakla. XmlSerializer instance'ları thread-safe olduğu için paylaşmak güvenli.
Güvenli Varsayılanlar: DTD, XmlResolver ve XXE
XML'in DTD (Document Type Definition; belgenin içinde entity ve yapı tanımlamaya yarayan eski şema mekanizması) özelliği, entity tanımlarıyla dış kaynaklara referans vermeye izin verir. XXE (XML External Entity; dış entity tanımı üzerinden yapılan saldırı) saldırısı bunu kullanarak sunucuya kendi dosyalarını okutur ya da iç ağa istek attırır; entity'lerin iç içe büyütülmesi ise belleği tüketen bir DoS (Denial of Service; servisi kullanılamaz hâle getiren saldırı) yaratabilir. Saldırının ayrıntısı ve örnekleri OWASP (Open Web Application Security Project; web güvenliği üzerine çalışan açık topluluk) Top 10 serisinin XXE'yi anlatan iki yazısında (A02 Security Misconfiguration ve 2017 listesine bakan yazı); burada yalnız .NET'in varsayılanlarına bakıyoruz. Hiçbir dış kaynağa referans vermeyen, zararsız bir iç entity içeren belgeyi üç API ile okuduk:
// Zararsız bir iç DTD: hiçbir dış referans yok.
const string withDtd = """
<?xml version="1.0"?>
<!DOCTYPE ShipmentNotice [ <!ENTITY carrier "ACME Cargo"> ]>
<ShipmentNotice><Carrier>&carrier;</Carrier></ShipmentNotice>
""";
XmlReaderSettings defaults: DtdProcessing=Prohibit
XmlReader.Create (default): XmlException: For security reasons DTD is prohibited in this XML document. To enable DTD processing set the DtdProcessing property on XmlReaderSettings to Parse and pass the settings into XmlReader.Create method.
XDocument.Parse: ACME Cargo
XmlDocument.LoadXml: ACME Cargo
MaxCharactersFromEntities default: 10,000,000
Üçüncü sorunun cevabı: hayır. XmlReader.Create'in varsayılanı DTD'yi tamamen reddetmek. XDocument ve XmlDocument ise DTD'yi işliyor ve iç entity'yi açıyor. Onları güvenli kılan, .NET (Core) ve .NET 10'da varsayılan XmlResolver'ın dış kaynakları çözmemesi: kendi makinemizde, bir dış dosyaya referans veren entity'nin bu iki API'de de boş döndüğünü ayrıca doğruladık. Entity açılımından gelen karakter sayısı da varsayılan olarak 10 milyonla sınırlı. Ama bu güvenlik tek bir satırla kaybolur: koda XmlResolver = new XmlUrlResolver() eklemek ya da DtdProcessing.Parse'ı resolver ile birlikte açmak dış entity'leri yeniden çözülebilir hâle getirir.
Asıl risk eski kodda. .NET Framework'ün 4.5.2'den önceki sürümlerinde XmlDocument'in varsayılan resolver'ı dış kaynakları çözüyordu ve XmlTextReader gibi eski API'ler DTD'yi varsayılan olarak işliyordu. Bu kodu .NET 10'a taşıdıktan sonra da açıkça yazılmış XmlResolver atamaları olduğu gibi kalır. Kural: dışarıdan gelen XML'i her zaman DtdProcessing = DtdProcessing.Prohibit ve XmlResolver = null verilmiş bir XmlReader üzerinden oku; XDocument.Load(XmlReader) ve XmlDocument.Load(XmlReader) overload'ları bunu mümkün kılar. UBL belgeleri yapılarını XSD ile tanımlar, DTD kullanmaz; DTD içeren bir fatura zaten şüphelidir.
Üretimde Dikkat
Encoding'i belgeye bırak. Bir XML dosyasını File.ReadAllText ile string'e okuyup sonra parse etmek, belgenin kendi encoding bildirimini atlar; Windows-1254 ile yazılmış eski bir banka dosyasında Türkçe karakterler bozulur. XmlReader'a doğrudan stream ver, encoding'i XML bildiriminden ve BOM'dan (Byte Order Mark; dosyanın başındaki kodlama işareti) kendisi çözsün.
Boyut sınırı koy. Kullanıcıdan gelen XML için request body limiti, XmlReaderSettings.MaxCharactersInDocument ve gerekiyorsa derinlik kontrolü kullan. Hibrit okuma belleği korur ama sonsuz büyüklükte bir dosyayı işlemeye çalışmaktan korumaz.
Şemayı bir kez yükle. XmlSchemaSet'i derlemek pahalıdır; her istekte yeniden yüklemek yerine uygulama açılırken bir kez derleyip paylaş.
Doğrulama hatalarını sınıflandır. İş ortağından gelen belgelerde şema hatası "geri gönder", iş kuralı hatası (toplam satırların toplamına eşit değil gibi) "incele" demektir. İkisini aynı exception ile loglamak, hangisinin arttığını görmeni zorlaştırır.
Sık Düşülen Tuzaklar
Namespace'i unutmak. Elements("InvoiceLine") namespace'li bir belgede sessizce boş döner. XNamespace sabitleri tanımla ve her sorguda kullan.
XmlSerializer'ı cache'lemeden XmlRootAttribute ya da XmlAttributeOverrides ile yaratmak. Her çağrı boşaltılamayan yeni bir assembly üretir. Bir kez yarat, sakla.
XmlResolver'ı "çalışmıyor" diye açmak. Bir şemanın import ettiği dosyayı bulmak için resolver açan kod, aynı reader'dan geçen kullanıcı XML'inin dış entity'lerini de çözer. Şemaları bizim yaptığımız gibi XmlSchemaSet'e tek tek ekle; kullanıcı XML'ini okuyan reader'da resolver hep null kalsın.
Büyük dosyayı XDocument.Load ile açmak. 267 MB'lık dosya 703 MB heap demekti. Bir belgeden fazlasını içeren her dosyada hibrit okuma.
Ondalık sayıları culture ile parse etmek. decimal.Parse(element.Value) Türkçe culture'da 299.80'i yanlış okur; explicit cast ya da XmlConvert.ToDecimal kullan.
Küçük Bir Deneme
★☆☆ read komutunu, satırları Descendants(cac + "InvoiceLine") yerine XPath ile bulacak şekilde değiştir (XPathSelectElements ve bir XmlNamespaceManager). Prefix'leri namespace manager'a kaydetmezsen ne oluyor?
★★☆ Şemaya bir iş kuralı ekleyemeyeceğin için kodla ekle: doğrulamadan sonra LineExtensionAmount'ların toplamının PayableAmount'a eşit olduğunu kontrol eden bir adım yaz ve farkı satır numarasıyla raporla. İpucu: LoadOptions.SetLineInfo ve IXmlLineInfo.
★★★ XmlSerializer için tip ve kök adına göre anahtarlı, thread-safe bir cache yaz ve serializer-cache komutundaki ikinci döngüyü onunla tekrar çalıştır. Dinamik assembly sayısı kaç oluyor?
XML'i bitirdik. Bir sonraki yazıda bir başka gündelik formata geçiyoruz: CSV (Comma-Separated Values; değerleri ayırıcıyla sıralayan düz metin formatı). Standardı RFC (Request for Comments; internet standartlarının yayımlandığı belge serisi) 4180'in neyi tanımlayıp neyi tanımlamadığına, Türkçe Excel'in noktalı virgül ve ondalık virgül alışkanlıklarına, Windows-1254 ile gelen eski banka dosyalarına ve CSV injection'a bakacağız.