Mert Özen Her Satırda Daha İleri
Backend C# ile Günlük .NET API'leri — #05

Encoding, Base64, Hex ve URI: UTF-8, BOM, Windows-1254, Base64Url, Uri ve URL Encoding

Mert Özen 15 Kas 2026 14 dk 9 görüntülenme
Encoding, Base64, Hex ve URI: UTF-8, BOM, Windows-1254, Base64Url, Uri ve URL Encoding

String ile byte arasındaki her geçişte encoding sözleşmenin parçasıdır: yanlış encoding Türkçe harfleri sessizce bozar, BOM CSV başlığına görünmez bir karakter ekler, Base64 ise şifreleme değildir. Bookstore'un eski muhasebe sisteminden gelen Windows-1254 verisi ve arama linki üzerinden Encoding, CodePagesEncodingProvider, Base64Url, ToHexStringLower, URL encoding API'lerinin farkları, QueryHelpers ve Uri'nin tuzaklarını gerçek çıktılarla gösteriyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ile derlenip denenmiştir; Base64 bölümündeki yeni API örneği .NET 11 SDK ile çalıştırılmıştır.

Önce Tahmin Et

Bookstore'un (sitedeki serilerin ortak örnek uygulaması, bir online kitapçı) katalogunda "Şeker Portakalı" diye bir kitap var. Üç satırın çıktısını tahmin et; üçüncüsünde ayrıca şunu düşün: bu string bir şifreyi koruyor mu?

Console.WriteLine(Title.Length);
Console.WriteLine(Encoding.UTF8.GetByteCount(Title));
Console.WriteLine(Convert.ToBase64String(Encoding.UTF8.GetBytes("Ada:example-password-change-me")));
15
17
QWRhOmV4YW1wbGUtcGFzc3dvcmQtY2hhbmdlLW1l

15 karakterlik başlık 17 byte tutuyor (UTF-8; Unicode karakterlerini 1 ile 4 byte arasında yazan kodlama), çünkü Ş ve ı iki byte'la yazılıyor. Veritabanı kolonunun ya da bir protokol alanının sınırı byte cinsindense, karakter sayısına bakarak yapılan kontrol yanlış sonuç verir. Üçüncü satır HTTP Basic authentication header'ında gönderilen türden bir değer ve hiçbir şeyi korumuyor: Base64 bir encoding, şifreleme değil. Bu yazının ilerisinde aynı string'i tek satırda geri çözeceğiz.

Serinin ilk yazılarında metni string olarak işledik. Bu yazıda string ile byte arasındaki sınıra bakıyoruz: Encoding sınıfı, BOM (Byte Order Mark; dosyanın başına konan ve kodlamayı belirten işaret byte'ları), eski sistemlerden gelen Windows-1254 verisi, Base64, Base64Url ve hex, son olarak URL encoding ve Uri. Dosya ve stream tarafı (StreamReader'ın BOM algılaması, Türkçe Excel'in ürettiği CSV (Comma-Separated Values; değerleri ayırıcıyla sıralayan düz metin formatı) dosyaları) .NET'te I/O İşlemleri serisinin "CSV ile Çalışmak" yazısında; burada byte[] ile string arasındaki dönüşümü ve API sınırlarını işliyoruz.

dotnet run --project samples/Bookstore.Bcl.Samples -- encoding-counts --culture en-US

Karakter, Byte ve Encoding

.NET'te string bellekte UTF-16 kod birimleri olarak tutulur. Bir string'i dosyaya, ağa ya da veritabanına yazmak için onu bir encoding ile byte'a çevirmek gerekir; okurken de aynı encoding ile geri çevirmek gerekir. Aynı başlığı beş encoding ile byte'a çevirip geri okudum:

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Encoding[] encodings = [Encoding.UTF8, Encoding.Unicode, Encoding.Latin1, Encoding.ASCII, Encoding.GetEncoding(1254)];

Console.WriteLine($"\"{Title}\" has {Title.Length} chars");
foreach (Encoding encoding in encodings)
{
    byte[] bytes = encoding.GetBytes(Title);
    string back = encoding.GetString(bytes);
    Console.WriteLine($"{encoding.WebName,-12} {encoding.GetByteCount(Title),3} bytes  {Convert.ToHexString(bytes[..4]),-9}...  round-trip: {back}");
}
"Şeker Portakalı" has 15 chars
utf-8         17 bytes  C59E656B ...  round-trip: Şeker Portakalı
utf-16        30 bytes  5E016500 ...  round-trip: Şeker Portakalı
iso-8859-1    15 bytes  53656B65 ...  round-trip: Seker Portakali
us-ascii      15 bytes  3F656B65 ...  round-trip: ?eker Portakal?
windows-1254  15 bytes  DE656B65 ...  round-trip: Şeker Portakalı

UTF-8, ASCII (İngilizce harfler, rakamlar ve temel işaretlerden oluşan 128 karakterlik set) karakterleri tek byte'ta, Türkçe harfleri iki byte'ta tutar ve bütün Unicode'u kapsar; dosya, JSON, HTTP ve veritabanı için varsayılan seçim budur. Encoding.Unicode adına rağmen UTF-16 little-endian'dır ve her karakter için en az iki byte harcar. Asıl ders son üç satırda. Latin-1'de (iso-8859-1, Batı Avrupa dilleri için 256 karakterlik tek byte'lık kodlama) Ş ve ı yok; .NET onları hata vermeden S ve i'ye çevirdi ve geri okuduğumuzda "Seker Portakali" kaldı. ASCII aynı karakterleri ? yaptı. Veri sessizce bozuldu ve hiçbir exception yok. Windows-1254 ise Türkçe harfleri tek byte'ta tutuyor, bu yüzden 15 byte ve doğru geri dönüş.

Encoding.GetEncoding(1254) .NET Core'da varsayılan olarak çalışmaz; Windows'a özgü code page'ler CodePagesEncodingProvider ile kaydedildikten sonra kullanılabilir. Provider .NET'in paylaşılan framework'ünde geliyor, ayrıca paket eklemen gerekmiyor; uygulama açılırken bir kez Encoding.RegisterProvider çağırman yeterli. Kaydetmezsen NotSupportedException ya da ArgumentException alırsın.

BOM: Görünmeyen Üç Byte

UTF-8 dosyalar başlarında EF BB BF byte'larını taşıyabilir. Bu byte'lar UTF-8 için bir sıra bilgisi taşımaz, yalnız "bu dosya UTF-8" işaretidir. Excel ve bazı Windows araçları bunu bekler, Unix araçları ise genelde istemez. Muhasebeye giden sipariş dosyasını BOM'lu yazıp byte olarak geri okuyalım:

byte[] preamble = Encoding.UTF8.GetPreamble();
Console.WriteLine($"Encoding.UTF8 preamble: {Convert.ToHexString(preamble)} ({preamble.Length} bytes)");
Console.WriteLine($"GetBytes adds BOM: {Encoding.UTF8.GetBytes("A").Length == 4}");

File.WriteAllText(path, "OrderNumber,Total\n", new UTF8Encoding(encoderShouldEmitUTF8Identifier: true));
byte[] fromExcel = File.ReadAllBytes(path);
Console.WriteLine($"File starts with: {Convert.ToHexString(fromExcel[..6])}");

string naive = Encoding.UTF8.GetString(fromExcel);
string header = naive.Split(',')[0];
Console.WriteLine($"GetString header: \"{header}\" length={header.Length} == \"OrderNumber\": {header == "OrderNumber"}");
Console.WriteLine($"First char: U+{(int)header[0]:X4}");

string viaReader = File.ReadAllText(path);
Console.WriteLine($"File.ReadAllText header == \"OrderNumber\": {viaReader.Split(',')[0] == "OrderNumber"}");

ReadOnlySpan<byte> body = fromExcel.AsSpan().StartsWith(preamble) ? fromExcel.AsSpan(preamble.Length) : fromExcel;
Console.WriteLine($"Skip preamble, then GetString: {Encoding.UTF8.GetString(body).Split(',')[0] == "OrderNumber"}");
Encoding.UTF8 preamble: EFBBBF (3 bytes)
GetBytes adds BOM: False
File starts with: EFBBBF4F7264
GetString header: "OrderNumber" length=12 == "OrderNumber": False
First char: U+FEFF
File.ReadAllText header == "OrderNumber": True
Skip preamble, then GetString: True

Çıktıdaki "OrderNumber" ekranda doğru görünüyor ama uzunluğu 12: başında görünmez U+FEFF karakteri var. Encoding.GetString BOM'u tanımaz, onu metnin parçası olarak çözer; "OrderNumber" kolonunu adıyla arayan import kodu kolonu bulamaz ve hata mesajında da her şey normal görünür. File.ReadAllText ve StreamReader ise BOM'u algılayıp atar. Kural: byte dizisi bir dosyadan ya da HTTP gövdesinden geliyorsa ya reader kullan ya da preamble'ı kendin atla. Yazarken de bilinçli seç: Encoding.UTF8 instance'ı stream'e yazarken BOM ekler, new UTF8Encoding(false) eklemez; File.WriteAllText'in encoding almayan overload'u BOM'suz UTF-8 yazar. Karşı tarafın ne beklediğini (Excel mi, bir Linux script'i mi) bilmeden karar verme.

Bookstore: Eski Muhasebe Sisteminden Gelen Windows-1254

Bookstore'un muhasebe entegrasyonu hâlâ eski bir sistemden müşteri adı alanları alıyor. Sistem Türkçe Windows için yazılmış ve metni Windows-1254 code page'iyle saklıyor. Aynı byte'ları farklı encoding'lerle okuyunca:

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
Encoding turkish = Encoding.GetEncoding(1254);

// Eski muhasebe sisteminin dışa aktardığı müşteri adı alanı (Windows-1254).
byte[] legacy = [0xDE, 0x65, 0x6B, 0x65, 0x72, 0x20, 0x50, 0x6F, 0x72, 0x74, 0x61, 0x6B, 0x61, 0x6C, 0xFD];

Console.WriteLine($"as 1254  : {turkish.GetString(legacy)}");
Console.WriteLine($"as UTF-8 : {Encoding.UTF8.GetString(legacy)}");
Console.WriteLine($"as Latin1: {Encoding.Latin1.GetString(legacy)}");

byte[] utf8 = Encoding.UTF8.GetBytes(Title);
Console.WriteLine($"UTF-8 bytes read as 1254: {turkish.GetString(utf8)}");

byte[] converted = Encoding.Convert(turkish, Encoding.UTF8, legacy);
Console.WriteLine($"Encoding.Convert 1254 -> UTF-8: {legacy.Length} -> {converted.Length} bytes, {Encoding.UTF8.GetString(converted)}");
as 1254  : Şeker Portakalı
as UTF-8 : �eker Portakal�
as Latin1: Þeker Portakalý
UTF-8 bytes read as 1254: Åeker Portakalı
Encoding.Convert 1254 -> UTF-8: 15 -> 17 bytes, Şeker Portakalı

Türkçe karakter sorunlarının hemen hepsi bu dört satırdan biridir ve çıktıya bakarak hangisi olduğunu anlayabilirsin. � (U+FFFD, replacement character) görüyorsan 1254 verisi UTF-8 diye okunmuştur; UTF-8 bu byte'ları geçersiz sayıp yerine bu karakteri koyar. Þ ve ý görüyorsan aynı veri Latin-1 (ya da Windows-1252) ile okunmuştur; aynı byte değerleri o tabloda İzlanda harflerine karşılık gelir. ı ve Å gibi iki karakterlik gruplar görüyorsan tersi olmuştur: UTF-8 veri tek byte'lık bir code page ile okunmuştur ve her Türkçe harf iki karaktere bölünmüştür (Å'dan sonra ekranda görünmeyen bir kontrol karakteri daha var). Encoding.Convert byte dizisini bir encoding'den diğerine doğrudan çevirir; eski sistemden gelen alanı kendi tarafımızda hemen UTF-8 string'e çevirip içeride yalnız onu taşımak en sağlıklısı.

Varsayılan davranışın sessizce bozması tehlikeli; bozuk veriyi sisteme girmeden yakalamak için exception fallback kullanabilirsin:

var strictUtf8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
try
{
    strictUtf8.GetString(legacy);
}
catch (DecoderFallbackException ex)
{
    Console.WriteLine($"{ex.GetType().Name}: {ex.Message.Split('\n')[0]}");
}

Encoding strictAscii = Encoding.GetEncoding("us-ascii", EncoderFallback.ExceptionFallback, DecoderFallback.ExceptionFallback);
Console.WriteLine($"ASCII (default fallback): {Encoding.ASCII.GetString(Encoding.ASCII.GetBytes(Title))}");
try
{
    strictAscii.GetBytes(Title);
}
catch (EncoderFallbackException ex)
{
    Console.WriteLine($"{ex.GetType().Name}: {ex.Message.Split('\n')[0]}");
}
DecoderFallbackException: Unable to translate bytes [DE] at index 0 from specified code page to Unicode.
ASCII (default fallback): ?eker Portakal?
EncoderFallbackException: Unable to translate Unicode character \\u015E at index 0 to specified code page.

Hangi byte'ın hangi index'te sorun çıkardığını söyleyen bir exception, import job'ının log'unda Portakal� görmekten çok daha kullanışlı. Import'u satır bazında yapıyorsan hatalı satırı ayrı bir dosyaya yazıp devam etmek, bütün dosyayı reddetmekten genelde daha iyi bir iş kuralıdır.

Base64, Base64Url ve Hex

Base64 rastgele byte'ları yalnız harf, rakam, +, / ve = içeren bir metne çevirir; her 3 byte 4 karakter olur. Amacı byte'ı metin taşıyan bir kanaldan (JSON, e-posta, HTTP header) geçirmek. Gizlilik sağlamaz:

string basic = Convert.ToBase64String(Encoding.UTF8.GetBytes("Ada:example-password-change-me"));
Console.WriteLine($"Base64 : {basic}");
Console.WriteLine($"Decoded: {Encoding.UTF8.GetString(Convert.FromBase64String(basic))}");

byte[] token = [0xFB, 0xEF, 0xFF, 0x3E, 0x3F, 0x00, 0x10];
string standard = Convert.ToBase64String(token);
string urlSafe = Base64Url.EncodeToString(token);
Console.WriteLine($"Base64    : {standard}");
Console.WriteLine($"Base64Url : {urlSafe}");
Console.WriteLine($"Round-trip: {Base64Url.DecodeFromChars(urlSafe).AsSpan().SequenceEqual(token)}");
Console.WriteLine($"Standard Base64 in a query string: {Uri.EscapeDataString(standard)}");

try
{
    Convert.FromBase64String(urlSafe);
}
catch (FormatException ex)
{
    Console.WriteLine($"FromBase64String(Base64Url) -> {ex.GetType().Name}: {ex.Message}");
}
Base64 : QWRhOmV4YW1wbGUtcGFzc3dvcmQtY2hhbmdlLW1l
Decoded: Ada:example-password-change-me
Base64    : ++//Pj8AEA==
Base64Url : --__Pj8AEA
Round-trip: True
Standard Base64 in a query string: %2B%2B%2F%2FPj8AEA%3D%3D
FromBase64String(Base64Url) -> FormatException: The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters.

İlk iki satır yazının başındaki sorunun cevabı: Basic authentication'daki kullanıcı adı ve şifre tek bir method çağrısıyla geri okunuyor; koruma TLS'ten (Transport Layer Security; HTTPS'in şifreleme katmanı) gelir, Base64'ten değil. Bir değeri gerçekten gizlemek gerekiyorsa şifreleme, bütünlüğünü korumak gerekiyorsa HMAC (Hash-based Message Authentication Code; anahtarlı bir hash ile mesajın değiştirilmediğini doğrulayan yöntem) gerekir; ikisi de Geliştirici için Kriptografi serisinde.

Standart Base64'teki +, / ve = URL'de özel anlam taşır; query string'e konunca %2B%2B%2F%2F... gibi uzayan ve yanlış decode edilmeye açık bir hâle gelir (+ bazı sunucularda boşluk olarak okunur). .NET 9 ile gelen System.Buffers.Text.Base64Url RFC (Request for Comments; internet standartlarının yayımlandığı belge serisi) 4648'deki URL güvenli alfabeyi kullanır: + yerine -, / yerine _ ve padding yok. Sipariş takip linki ya da e-posta doğrulama token'ı için doğru seçim bu. İki format birbirinin yerine geçmez; Base64Url bir değeri Convert.FromBase64String'e verirsen FormatException alırsın.

Hex: ToHexString ve ToHexStringLower

Hash değerlerini, dosya imzalarını ya da byte dökümlerini okumak için hex kullanılır; her byte iki karakter olur:

byte[] hash = System.Security.Cryptography.SHA256.HashData(Encoding.UTF8.GetBytes("9780441172719"));
Console.WriteLine($"ToHexString      : {Convert.ToHexString(hash[..8])}");
Console.WriteLine($"ToHexStringLower : {Convert.ToHexStringLower(hash[..8])}");
Console.WriteLine($"BitConverter     : {BitConverter.ToString(hash[..8])}");
Console.WriteLine($"FromHexString    : {Convert.FromHexString("DE656B6572").Length} bytes, {Encoding.GetEncoding(1254).GetString(Convert.FromHexString("DE656B6572"))}");
Console.WriteLine($"255.ToString(\"x2\"): {255.ToString("x2")}");
ToHexString      : 9B07AAACA15F6202
ToHexStringLower : 9b07aaaca15f6202
BitConverter     : 9B-07-AA-AC-A1-5F-62-02
FromHexString    : 5 bytes, Şeker
255.ToString("x2"): ff

Convert.ToHexString .NET 5'ten beri var ve büyük harf üretir. Küçük harf için eskiden ToHexString(...).ToLowerInvariant() yazıp ikinci bir string ayırıyorduk; .NET 9'daki ToHexStringLower bunu tek adımda yapıyor. ETag, dosya özeti ya da Git commit kimliği gibi değerler genelde küçük harfle karşılaştırılır. BitConverter.ToString'in tireli çıktısı yalnız insan okuması için; Convert.FromHexString onu kabul etmez.

.NET 11: Base64 sınıfına gelen string ve char API'leri

System.Buffers.Text.Base64 sınıfı şimdiye kadar yalnız UTF-8 byte'larla çalışıyordu. .NET 11 ona Base64Url'deki gibi string ve char tabanlı method'lar ekledi: EncodeToString, EncodeToChars, DecodeFromChars, EncodeToUtf8 ve DecodeFromUtf8. Hem allocation yapan kolay overload'lar hem de span alan allocation'sız overload'lar var. Bookstore.Bcl.Net11 projesinde:

string encoded = System.Buffers.Text.Base64.EncodeToString(token);
byte[] decoded = System.Buffers.Text.Base64.DecodeFromChars(encoded);
Console.WriteLine($"Base64.EncodeToString : {encoded}");
Console.WriteLine($"Base64.DecodeFromChars: {Convert.ToHexString(decoded)}");
Console.WriteLine($"Same as Convert       : {encoded == Convert.ToBase64String(token)}");
Base64.EncodeToString : ++//Pj8AEA==
Base64.DecodeFromChars: FBEFFF3E3F0010
Same as Convert       : True

Sonuç Convert.ToBase64String ile aynı; fark API şeklinde. Base64 ve Base64Url artık aynı method adlarıyla kullanılıyor, iki format arasında geçiş tek bir sınıf adını değiştirmek kadar kolay.

URL Encoding ve Query String Kurmak

URL'de boşluk, #, &, ?, / ve ASCII dışı karakterler özel anlam taşır ya da hiç yer alamaz; bu yüzden değerler yüzde kodlamasıyla (%C5%9E) yazılır. .NET'te üç ayrı API var ve sonuçları aynı değil:

string[] inputs = [Title, "C# in Depth", "Dune & Children of Dune", "50% off/new?"];
Console.WriteLine($"{"input",-26}{"Uri.EscapeDataString",-50}{"WebUtility.UrlEncode",-46}HttpUtility.UrlEncode");
foreach (string input in inputs)
    Console.WriteLine($"{input,-26}{Uri.EscapeDataString(input),-50}{WebUtility.UrlEncode(input),-46}{HttpUtility.UrlEncode(input)}");
input                     Uri.EscapeDataString                              WebUtility.UrlEncode                          HttpUtility.UrlEncode
Şeker Portakalı           %C5%9Eeker%20Portakal%C4%B1                       %C5%9Eeker+Portakal%C4%B1                     %c5%9eeker+Portakal%c4%b1
C# in Depth               C%23%20in%20Depth                                 C%23+in+Depth                                 C%23+in+Depth
Dune & Children of Dune   Dune%20%26%20Children%20of%20Dune                 Dune+%26+Children+of+Dune                     Dune+%26+Children+of+Dune
50% off/new?              50%25%20off%2Fnew%3F                              50%25+off%2Fnew%3F                            50%25+off%2fnew%3f

Uri.EscapeDataString RFC 3986'ya uyar: boşluk %20, hex büyük harf. WebUtility.UrlEncode ve HttpUtility.UrlEncode HTML formlarının application/x-www-form-urlencoded kuralını uygular: boşluk +. HttpUtility ayrıca hex'i küçük harf yazar. Üçü de sunucu tarafında doğru çözülür, ama path segmentinde + boşluk sayılmaz ve imzalı URL'lerde (byte byte karşılaştırılan) büyük-küçük harf farkı imzayı bozar. Varsayılan seçim olarak Uri.EscapeDataString kullan; form gövdesi üretiyorsan FormUrlEncodedContent bunu senin için yapar.

Bookstore'un arama sayfasına link üreten kodda en sık görülen hata string birleştirme. Üç kitap adıyla denedim:

const string baseUrl = "https://bookstore.example.com/search";
string[] queries = [Title, "C# in Depth", "Dune & Children of Dune"];

foreach (string q in queries)
{
    string concatenated = $"{baseUrl}?q={q}&lang=tr";
    var parsed = new Uri(concatenated);
    string? qValue = HttpUtility.ParseQueryString(parsed.Query)["q"];
    Console.WriteLine($"concat : {concatenated}");
    Console.WriteLine($"         server sees q=\"{qValue}\", fragment=\"{parsed.Fragment}\"");

    string built = QueryHelpers.AddQueryString(baseUrl, new Dictionary<string, string?> { ["q"] = q, ["lang"] = "tr" });
    string? builtValue = HttpUtility.ParseQueryString(new Uri(built).Query)["q"];
    Console.WriteLine($"helper : {built}");
    Console.WriteLine($"         server sees q=\"{builtValue}\"");
}
concat : https://bookstore.example.com/search?q=Şeker Portakalı&lang=tr
         server sees q="Şeker Portakalı", fragment=""
helper : https://bookstore.example.com/search?q=%C5%9Eeker%20Portakal%C4%B1&lang=tr
         server sees q="Şeker Portakalı"
concat : https://bookstore.example.com/search?q=C# in Depth&lang=tr
         server sees q="C", fragment="#%20in%20Depth&lang=tr"
helper : https://bookstore.example.com/search?q=C%23%20in%20Depth&lang=tr
         server sees q="C# in Depth"
concat : https://bookstore.example.com/search?q=Dune & Children of Dune&lang=tr
         server sees q="Dune ", fragment=""
helper : https://bookstore.example.com/search?q=Dune%20%26%20Children%20of%20Dune&lang=tr
         server sees q="Dune & Children of Dune"

İlk örnek tesadüfen çalışıyor: Uri ASCII dışı karakterleri ve boşluğu kendisi kodladı. İkincisinde # fragment'in başlangıcı sayıldı; sunucu yalnız "C" aldı, lang parametresi hiç gitmedi (fragment tarayıcıda kalır, sunucuya gönderilmez). Üçüncüsünde & parametre ayırıcı sayıldı ve arama "Dune " oldu. Kullanıcı girdisini URL'e birleştirmek aynı zamanda bir güvenlik sorunu: girdiye &admin=true ya da # ekleyerek URL'in anlamını değiştirmek mümkün. QueryHelpers.AddQueryString her değeri Uri.EscapeDataString kuralıyla kodlar ve var olan query string'e doğru ayırıcıyla ekler. ASP.NET Core paylaşılan framework'ünde (Microsoft.AspNetCore.WebUtilities) geldiği için örnek console projesine <FrameworkReference Include="Microsoft.AspNetCore.App" /> ekledik; Web API projesinde zaten var.

Uri ve UriBuilder

Uri bir adresi parse eder, parçalarına ayırır ve normalize eder. Birkaç davranışı bilmek gerekiyor:

var uri = new Uri("https://bookstore.example.com/search?q=%C5%9Eeker%20Portakal%C4%B1&lang=tr");
Console.WriteLine($"ToString()     : {uri}");
Console.WriteLine($"AbsoluteUri    : {uri.AbsoluteUri}");
Console.WriteLine($"OriginalString : {uri.OriginalString}");
Console.WriteLine($"Host={uri.Host} Path={uri.AbsolutePath} Query={uri.Query}");

var relative = new Uri(new Uri("https://bookstore.example.com/books/"), "../authors/ada");
Console.WriteLine($"Combine        : {relative}");

Console.WriteLine($"TryCreate(\"bookstore.example.com/books\"): {Uri.TryCreate("bookstore.example.com/books", UriKind.Absolute, out _)}");
Console.WriteLine($"TryCreate(\"javascript:alert(1)\"): {Uri.TryCreate("javascript:alert(1)", UriKind.Absolute, out Uri? js)} scheme={js?.Scheme}");

var builder = new UriBuilder("https", "bookstore.example.com")
{
    Path = "books/kurk-mantolu-madonna",
    Query = $"ref={Uri.EscapeDataString("newsletter 11/2026")}",
};
Console.WriteLine($"UriBuilder: {builder.Uri.AbsoluteUri}");
ToString()     : https://bookstore.example.com/search?q=Şeker Portakalı&lang=tr
AbsoluteUri    : https://bookstore.example.com/search?q=%C5%9Eeker%20Portakal%C4%B1&lang=tr
OriginalString : https://bookstore.example.com/search?q=%C5%9Eeker%20Portakal%C4%B1&lang=tr
Host=bookstore.example.com Path=/search Query=?q=%C5%9Eeker%20Portakal%C4%B1&lang=tr
Combine        : https://bookstore.example.com/authors/ada
TryCreate("bookstore.example.com/books"): False
TryCreate("javascript:alert(1)"): True scheme=javascript
UriBuilder: https://bookstore.example.com/books/kurk-mantolu-madonna?ref=newsletter%2011%2F2026

ToString() kodlamayı geri açıp okunabilir bir metin üretiyor; içinde boşluk olduğu için bu artık geçerli bir URL değil. Log'a, e-postaya ya da başka bir isteğe URL koyarken AbsoluteUri kullan. Göreli bir adresi taban adresle birleştirmek için new Uri(baseUri, relative) overload'u .. segmentlerini de çözüyor; taban adresin sonundaki /'ın sonucu değiştirdiğini unutma. TryCreate şeması olmayan adresi mutlak kabul etmiyor, ama javascript: şemalı bir değeri geçerli bir URI (Uniform Resource Identifier; bir kaynağı tanımlayan adres, URL'in genel hâli) sayıyor. Kullanıcıdan gelen "web sitesi" ya da "geri dönüş adresi" alanını doğrularken şemanın Uri.UriSchemeHttps olduğunu ayrıca kontrol etmezsen, o değeri bir href'e koyduğun anda XSS (Cross-Site Scripting; sayfaya zararlı script enjekte etme) açığı açarsın; konu Web Güvenliği serisinin XSS yazısında. .NET 11 data: şeması için de Uri.UriSchemeData sabitini ekledi. UriBuilder parçaları ayrı ayrı vererek adres kurmanı sağlar, ama Query property'sine verdiğin değeri kendisi kodlamaz; parametreleri yine Uri.EscapeDataString ile kodlaman gerekir.

Üretimde Dikkat

Encoding'i sistemin kenarında bir kez belirle. Dışarıdan gelen her byte dizisinin (dosya, HTTP gövdesi, kuyruk mesajı, eski sistem alanı) encoding'i sözleşmenin parçası olmalı ve o sınırda string'e çevrilmeli. İçeride yalnız string dolaşsın, dışarı giderken de tek bir encoding (UTF-8) kullanılsın. "Encoding'i otomatik algılayan" kod yazmak cazip görünür, ama 1254 ile 1252 gibi tek byte'lık code page'leri byte'lara bakarak güvenilir şekilde ayırt etmenin yolu yok.

Uzunluk sınırlarını doğru birimle kontrol et. SQL Server'da nvarchar(200) 200 UTF-16 kod birimi, varchar(200) ise collation'a göre 200 byte demektir; UTF-8 collation'lı bir varchar kolonuna 200 karakterlik Türkçe başlık sığmayabilir. Bir HTTP header'ı ya da bir protokol alanı byte sınırı koyuyorsa Encoding.UTF8.GetByteCount ile kontrol et.

URL'leri her zaman bir builder ile kur ve log'a AbsoluteUri yaz. Token'ları ve imzalı linkleri Base64Url ile taşı; Base64'ü bir URL'e koymak zorunda kalırsan önce Uri.EscapeDataString'ten geçir. Bu sınırlarda yapılan küçük hatalar, yalnız belli kitap adlarında ya da belli müşterilerde görünen ve tekrarlanması zor hatalara dönüşür.

Sık Düşülen Tuzaklar

Base64'ü şifreleme sanmak. Base64'lü bir değer tek satırda geri çözülür; Basic authentication header'ı, log'a yazılan bir "şifrelenmiş" alan ya da URL'deki bir id hiçbir gizlilik sağlamaz.

URL'e string birleştirme ile parametre eklemek. # ve & içeren bir kitap adı aramayı bozar, kötü niyetli bir girdi URL'in anlamını değiştirir. QueryHelpers.AddQueryString ya da Uri.EscapeDataString kullan.

BOM'un CSV başlığını bozması. Encoding.GetString BOM'u U+FEFF karakteri olarak bırakır, ilk kolonun adı eşleşmez ve hata mesajı normal görünür. Reader kullan ya da preamble'ı atla.

Latin-1 ya da ASCII ile Türkçe metin yazmak. Exception oluşmaz; Ş sessizce S'ye ya da ?'e döner. Kayıpsız dönüşüm gerekiyorsa exception fallback kullan.

Uri.ToString()'i URL olarak kullanmak. Kodlanmış karakterleri geri açar ve geçersiz bir adres üretir; AbsoluteUri kullan.

Küçük Bir Deneme

★☆☆ encoding-1254 örneğine aynı byte'ları Encoding.GetEncoding(1252) ile okuyan bir satır ekle. Latin-1 çıktısıyla karşılaştır ve farklı çıkan karakteri bul.

★★☆ Bookstore için bir LegacyFieldReader yaz: 1254 kodlu bir byte dizisini exception fallback ile string'e çevirsin, hata olursa satır numarasıyla birlikte loglayıp null dönsün. Bozuk bir byte içeren test verisiyle dene.

★★★ Sipariş takip linki için bir token formatı tasarla: 16 byte rastgele değer (RandomNumberGenerator) ve sipariş numarası, ikisi birlikte Base64Url ile kodlansın. Token'ı üreten ve geri çözen iki method yaz, QueryHelpers.AddQueryString ile linke ekle ve linkin hiçbir karakterinin yeniden kodlanmadığını doğrula. Token'ın değiştirilmesini önlemek için ne eklemen gerektiğini Kriptografi serisindeki HMAC yazısına bakarak bul.

Byte'lardan sonra zamana geçiyoruz. Bir sonraki yazıda TimeZoneInfo'nun Windows ve Linux'ta farklı id'lerle çalışması ("Turkey Standard Time" ve "Europe/Istanbul"), ParseExact ve DateTimeStyles, tarihin metne çevrilip kayıpsız geri okunması, iş günü ve ay sonu hesabı ile testte TimeProvider kullanımını göreceğiz.