TOML, INI, .env ve Diğerleri: Hangi Configuration Formatı Ne Zaman?
Dosyayı kimin okuyacağına göre seç: tek okuyucu .NET ise JSON ve environment variable yeterli, aynı .env'i birden fazla araç okuyorsa en katı ortak alt kümeyle yaz. Aynı .env'i Compose, Node.js, dotenv, python-dotenv ve bash'le; aynı INI'yi .NET, Python ve git'le okuyup farkları, TOML 1.1'in tarih tiplerini ve Bookstore.Api ayarının beş formatta tek configuration'a dönüşmesini gösteriyoruz.
Sürüm notu: Bu yazıdaki örnekler .NET 10 (Microsoft.Extensions.Configuration paketleri 10.0.15, NetEscapades.Configuration.Yaml 3.1.0, Tomlyn 2.11.1), Python 3.14 (tomli 2.5.0, python-dotenv 1.2.4), Node.js 26.13 (dotenv 18.0.10), git ve Docker Compose 5.1 ile çalıştırılmıştır. Spec olarak 1.1.0 sürümü (Aralık 2025) esas alınmıştır.
Önce Tahmin Et
Sitedeki serilerin ortak örnek uygulaması Bookstore'un (bir online kitapçı) API'sini yerelde çalıştırırken bazı ayarları bir .env dosyasından veriyoruz:
# Bookstore.Api yerel ayarları
SHIPPING_DEFAULT_COUNTRY=NO
SHIPPING_NOTE=Free shipping # over 500 TRY
SHIPPING_TRACKING_URL=https://bookstore.example/track#latest
SHIPPING_DOUBLE="first line\nsecond line"
SHIPPING_SINGLE='first line\nsecond line'
SHIPPING_MULTILINE="line one
line two"
SHIPPING_SPACES = padded
export SHIPPING_EXPORTED=yes
SHIPPING_TEMPLATE=${SHIPPING_DEFAULT_COUNTRY}-express
SHIPPING_EMPTY=
Bu dosyayı Docker Compose (env_file), Node.js'in --env-file seçeneği, npm'deki dotenv paketi, Python'daki python-dotenv ve bash'in source komutu okuyor. SHIPPING_TRACKING_URL'deki #latest ve SHIPPING_TEMPLATE'teki ${...} her araçta aynı mı çıkar? Hangisi dosyanın bir satırını komut olarak çalıştırmaya kalkar?
TOML 1.1: Tipli ve Açık Bir Config Formatı
TOML (Tom's Obvious Minimal Language; okunması kolay, tipleri açık bir config formatı) JSON'un yorumsuzluğuna ve YAML'ın (girintiye dayalı veri formatı) tip tahminine bir cevap olarak çıktı. Syntax'ı INI'ye (köşeli parantezli bölümlerden ve key=value satırlarından oluşan eski config formatı) benziyor: [Shipping] bir table (JSON'daki object) açar, altındaki key = value satırları o table'a girer. Farkı tiplerin açık olması: string'ler her zaman tırnaklı, true/false dışında boolean yok, sayılarda baştaki sıfır yasak. JSON ve YAML'dan ayrıldığı en önemli nokta ise tarih ve saat tiplerinin spec'te olması: offset'li tarih-saat, yerel tarih-saat, yerel tarih ve yerel saat. Bookstore.Api'nin kargo ayarları TOML'da:
# Bookstore.Api kargo ayarları (TOML 1.1)
[Shipping]
DefaultCountry = "NO"
Cutoff = 12:30
Express = true
MaxWeightGrams = 1_000
ApiVersion = "1.10"
Carriers = ["standard", "express"]
# array of tables: her [[...]] başlığı bir element ekler
[[Shipping.Rates]]
Region = "domestic"
Price = 49.90
[[Shipping.Rates]]
Region = "international"
Price = 349.90
[[Shipping.Rates]] array of tables: her başlık, Rates array'ine yeni bir table ekliyor. JSON'da bir object array'ine karşılık gelir. 1_000'deki alt çizgi okunabilirlik için; değer 1000. Aralık 2025'te yayımlanan TOML 1.1.0 dört şey ekledi: inline table'ların birden fazla satıra yayılabilmesi ve trailing comma, string'lerde \e ve \xHH escape'leri, saatte saniyenin isteğe bağlı olması. Son madde bu dosyayı ilgilendiriyor, çünkü Cutoff = 12:30 1.0'da geçersiz:
#:package Tomlyn@2.11.1
#:property PublishAot=false
using Tomlyn;
string text = File.ReadAllText("settings/shipping.toml");
Settings settings = TomlSerializer.Deserialize<Settings>(text)!;
ShippingSettings shipping = settings.Shipping;
// TOML'da gerçek tarih/saat tipleri var: Cutoff string olarak değil TimeOnly olarak gelir
Console.WriteLine($"Cutoff: {shipping.Cutoff:HH:mm} ({shipping.Cutoff.GetType().Name})");
Console.WriteLine($"MaxWeightGrams: {shipping.MaxWeightGrams}, ApiVersion: {shipping.ApiVersion}");
foreach (Rate rate in shipping.Rates)
{
Console.WriteLine($"Rate: {rate.Region} {rate.Price}");
}
class Settings
{
public ShippingSettings Shipping { get; set; } = new();
}
class ShippingSettings
{
public string DefaultCountry { get; set; } = "";
public TimeOnly Cutoff { get; set; }
public bool Express { get; set; }
public int MaxWeightGrams { get; set; }
public string ApiVersion { get; set; } = "";
public List<string> Carriers { get; set; } = [];
public List<Rate> Rates { get; set; } = [];
}
class Rate
{
public string Region { get; set; } = "";
public decimal Price { get; set; }
}
$ python read_toml.py settings/shipping.toml
tomllib TOMLDecodeError Expected newline or end of document after a statement (at line 4, column 12)
tomli 12:30:00 1000 {'Region': 'international', 'Price': 349.9}
$ dotnet run toml.cs
Cutoff: 12:30 (TimeOnly)
MaxWeightGrams: 1000, ApiVersion: 1.10
Rate: domestic 49.9
Rate: international 349.9
Python'un standart kütüphanesindeki tomllib (3.14) TOML 1.0'ı uyguluyor ve 12:30'da durdu; tomli 2.4 ve sonrası 1.1'i destekliyor ve değeri bir time olarak okudu. .NET'te Tomlyn 2.11 de 1.1'i destekliyor ve Cutoff'u doğrudan TimeOnly'ye bind etti; YAML ve JSON'da bu değer her zaman bir string'di. Bir dikkat noktası: TOML'da decimal tipi yok, ondalıklı sayılar IEEE'nin (elektrik ve elektronik mühendislerinin standart kuruluşu) 754 standardındaki double olarak tanımlı. Bu yüzden 49.90, decimal bir property'ye 49.9 olarak geldi; para tutarını TOML'da string ya da kuruş cinsinden integer olarak tutmak daha güvenli.
TOML'la en çok Rust'ın Cargo.toml'unda, Python projelerinin pyproject.toml'unda, araç sürümlerini yöneten mise'nin mise.toml'unda ve Starship prompt'unun ayar dosyasında karşılaşırsın. .NET'te ise yaygın değil: Microsoft'un TOML için bir configuration provider'ı yok. Tomlyn gibi bir kütüphaneyle dosyayı kendin okuyabilirsin.
INI Ailesi: Standart Olmayan Bir Standart
INI en eski ve en basit format. .editorconfig, .gitconfig, php.ini ve systemd unit dosyaları bu ailedendir. Sorun, INI'nin bir spec'i olmaması: her parser yorumu, tırnağı ve büyük/küçük harfi kendine göre yorumluyor. Aynı dosyayı üç parser'la okuyalım:
# aynı dosya, üç INI parser'ı tarafından okunuyor
[Shipping]
Note="Free shipping"
Code=TR ; is this a comment?
TrackingUrl=https://bookstore.example/track#latest
$ dotnet run ini.cs -- settings/quirks.ini
Shipping:Code = [TR ; is this a comment?]
Shipping:Note = [Free shipping]
Shipping:TrackingUrl = [https://bookstore.example/track#latest]
$ python -c 'import configparser; c = configparser.ConfigParser(); c.read("settings/quirks.ini"); print(dict(c["Shipping"]))'
{'note': '"Free shipping"', 'code': 'TR ; is this a comment?', 'trackingurl': 'https://bookstore.example/track#latest'}
$ git config --file settings/quirks.ini --list
shipping.note=Free shipping
shipping.code=TR
shipping.trackingurl=https://bookstore.example/track
Üç parser, üç sonuç. .NET'in INI provider'ı çift tırnakları atıyor, satır içindeki ; ve #'ı değerin parçası sayıyor. Python'un configparser'ı tırnakları koruyor ve key adlarını küçük harfe çeviriyor. git ise satır içindeki ; ve #'ı yorum başlangıcı sayıyor; takip URL'sinin #latest kısmı kayboldu. Kural: INI dosyasında satır sonuna yorum yazma, yorumu ayrı satıra koy ve değerde ; ya da # varsa o dosyayı okuyacak aracın dokümanına bak. .editorconfig'in kendi kuralları Coding Conventions ve Ekip Standartları serisinde.
.env: Herkesin Kullandığı, Kimsenin Tanımlamadığı Format
.env dosyasının da bir spec'i yok; her kütüphane "shell'e benzeyen" kendi kurallarını uyguluyor. Tahmindeki dosyanın sonuçları (Node script'i dotenv paketini kullanıyor; klasörde önce npm install çalıştır):
$ cat compose.yaml
name: bookstore
services:
api:
image: bookstore-api:dev
env_file: sample.env
$ docker compose config | yq -o json -I0 '.services.api.environment | pick(["SHIPPING_NOTE", "SHIPPING_TRACKING_URL", "SHIPPING_TEMPLATE", "SHIPPING_DOUBLE", "SHIPPING_SINGLE", "SHIPPING_SPACES", "SHIPPING_EXPORTED"])'
{"SHIPPING_NOTE":"Free shipping","SHIPPING_TRACKING_URL":"https://bookstore.example/track#latest","SHIPPING_TEMPLATE":"NO-express","SHIPPING_DOUBLE":"first line\nsecond line","SHIPPING_SINGLE":"first line\\nsecond line","SHIPPING_SPACES":"padded","SHIPPING_EXPORTED":"yes"}
$ node --env-file=sample.env show-env.mjs builtin
{"SHIPPING_NOTE":"Free shipping","SHIPPING_TRACKING_URL":"https://bookstore.example/track","SHIPPING_TEMPLATE":"${SHIPPING_DEFAULT_COUNTRY}-express","SHIPPING_DOUBLE":"first line\nsecond line","SHIPPING_SINGLE":"first line\\nsecond line","SHIPPING_SPACES":"padded","SHIPPING_EXPORTED":"yes"}
$ node show-env.mjs dotenv
{"SHIPPING_NOTE":"Free shipping","SHIPPING_TRACKING_URL":"https://bookstore.example/track","SHIPPING_TEMPLATE":"${SHIPPING_DEFAULT_COUNTRY}-express","SHIPPING_DOUBLE":"first line\nsecond line","SHIPPING_SINGLE":"first line\\nsecond line","SHIPPING_SPACES":"padded","SHIPPING_EXPORTED":"yes"}
$ python show_env.py
{"SHIPPING_NOTE": "Free shipping", "SHIPPING_TRACKING_URL": "https://bookstore.example/track#latest", "SHIPPING_TEMPLATE": "NO-express", "SHIPPING_DOUBLE": "first line\nsecond line", "SHIPPING_SINGLE": "first line\\nsecond line", "SHIPPING_SPACES": "padded", "SHIPPING_EXPORTED": "yes"}
$ bash -c 'set -a; source sample.env; set +a; echo "$SHIPPING_NOTE|$SHIPPING_TRACKING_URL"'
sample.env: line 3: shipping: command not found
sample.env: line 9: SHIPPING_SPACES: command not found
|https://bookstore.example/track#latest
Tahminin cevabı iki farkta. Birincisi #: Compose ve python-dotenv tırnaksız bir değerdeki #latest'i değerin parçası saydı, Node.js ve npm'deki dotenv ise #'tan sonrasını yorum olarak kesti. Aynı .env dosyasıyla Compose'da çalışan takip linki, Node.js ile çalışan bir script'te sessizce kısaldı. İkincisi ${...}: Compose ve python-dotenv değişkeni genişletti, Node.js ve dotenv olduğu gibi bıraktı. Bash ise dosyayı bir shell script'i olarak çalıştırdı: SHIPPING_NOTE=Free shipping satırını "SHIPPING_NOTE=Free environment'ıyla shipping komutunu çalıştır" diye okudu, eşittir etrafında boşluk olan satırı da komut sandı. Yani source .env yalnızca dosya bash syntax'ına tam uyuyorsa güvenli.
Ortak noktalar da var: dördü de tek tırnağın içindeki \n'i olduğu gibi bıraktı, çift tırnağın içindekini satır sonuna çevirdi, eşittirin etrafındaki boşluğu ve export önekini tolere etti. Taşınabilir bir .env için kural seti şu: değerde boşluk, #, $ veya ters bölü varsa tek tırnak kullan; satır sonu yorumu yazma; değişken genişletmeye güvenme; eşittirin etrafına boşluk koyma. Compose'un .env ile interpolation ve env_file arasındaki farkı Docker Compose serisinin configuration ve secret yazısında anlattık; burada yalnızca format açısından bakıyoruz.
Aynı Ayar, Beş Format, Tek Configuration
.NET'in configuration sistemi formatlardan bağımsız: her provider kendi formatını okuyup Shipping:DefaultCountry gibi düz key'lere çeviriyor, uygulama da bu key'leri bir options class'ına bind ediyor. Bookstore.Api'nin kargo ayarlarını JSON, YAML, INI ve environment variable olarak yazdık (TOML hâlini yukarıda gördük):
$ cat settings/shipping.json settings/shipping.yaml
{
"Shipping": {
"DefaultCountry": "NO",
"Cutoff": "12:30",
"Express": true,
"MaxWeightGrams": 1000,
"ApiVersion": "1.10",
"Carriers": ["standard", "express"]
}
}
# Bookstore.Api kargo ayarları (YAML)
Shipping:
DefaultCountry: NO
Cutoff: 12:30
Express: yes
MaxWeightGrams: 1000
ApiVersion: 1.10
Carriers:
- standard
- express
$ cat settings/shipping.ini settings/shipping.env
# Bookstore.Api kargo ayarları (INI)
[Shipping]
DefaultCountry=NO
Cutoff=12:30
Express=true
MaxWeightGrams=1000
ApiVersion=1.10
Carriers:0=standard
Carriers:1=express
# Bookstore.Api kargo ayarları (environment variable'lar)
Shipping__DefaultCountry=NO
Shipping__Cutoff=12:30
Shipping__Express=true
Shipping__MaxWeightGrams=1000
Shipping__ApiVersion=1.10
Shipping__Carriers__0=standard
Shipping__Carriers__1=express
Hiyerarşiyi her format kendi yoluyla kuruyor: JSON ve YAML'da iç içe yapı, INI'de [Shipping] bölümü ve :, environment variable'da çift alt çizgi (__), çünkü bazı platformlarda değişken adında : kullanılamıyor. Array'ler INI ve environment variable'da index ile yazılıyor (Carriers:0, Shipping__Carriers__0). Dosyaları okuyan program:
#:package Microsoft.Extensions.Configuration.Json@10.0.15
#:package Microsoft.Extensions.Configuration.Ini@10.0.15
#:package Microsoft.Extensions.Configuration.EnvironmentVariables@10.0.15
#:package Microsoft.Extensions.Configuration.Binder@10.0.15
#:package NetEscapades.Configuration.Yaml@3.1.0
#:package YamlDotNet@18.1.0
#:property PublishAot=false
using Microsoft.Extensions.Configuration;
string format = args[0];
ConfigurationBuilder builder = new();
builder.SetBasePath(Path.Combine(Directory.GetCurrentDirectory(), "settings"));
// her format için bir provider; ürettikleri key'ler aynı
_ = format switch
{
"json" => builder.AddJsonFile("shipping.json"),
"ini" => builder.AddIniFile("shipping.ini"),
"yaml" => builder.AddYamlFile("shipping.yaml"),
"env" => builder.AddEnvironmentVariables(),
_ => throw new ArgumentException($"unknown format {format}")
};
IConfigurationRoot config = builder.Build();
foreach ((string key, string? value) in config.GetSection("Shipping").AsEnumerable(makePathsRelative: false).OrderBy(p => p.Key))
{
if (value is not null)
{
Console.WriteLine($"{key} = {value}");
}
}
try
{
ShippingOptions options = config.GetSection("Shipping").Get<ShippingOptions>()!;
Console.WriteLine($"Bound: {options.DefaultCountry}, {options.Cutoff:HH:mm}, Express={options.Express}, {options.ApiVersion}, [{string.Join(", ", options.Carriers)}]");
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"Binding failed: {ex.Message}");
}
class ShippingOptions
{
public string DefaultCountry { get; set; } = "";
public TimeOnly Cutoff { get; set; }
public bool Express { get; set; }
public int MaxWeightGrams { get; set; }
public string ApiVersion { get; set; } = "";
public List<string> Carriers { get; set; } = [];
}
$ dotnet run config.cs -- json
Shipping:ApiVersion = 1.10
Shipping:Carriers:0 = standard
Shipping:Carriers:1 = express
Shipping:Cutoff = 12:30
Shipping:DefaultCountry = NO
Shipping:Express = True
Shipping:MaxWeightGrams = 1000
Bound: NO, 12:30, Express=True, 1.10, [standard, express]
$ dotnet run config.cs -- ini
Shipping:ApiVersion = 1.10
Shipping:Carriers:0 = standard
Shipping:Carriers:1 = express
Shipping:Cutoff = 12:30
Shipping:DefaultCountry = NO
Shipping:Express = true
Shipping:MaxWeightGrams = 1000
Bound: NO, 12:30, Express=True, 1.10, [standard, express]
$ dotnet run config.cs -- yaml
Shipping:ApiVersion = 1.10
Shipping:Carriers:0 = standard
Shipping:Carriers:1 = express
Shipping:Cutoff = 12:30
Shipping:DefaultCountry = NO
Shipping:Express = yes
Shipping:MaxWeightGrams = 1000
Binding failed: Failed to convert configuration value 'yes' at 'Shipping:Express' to type 'System.Boolean'.
$ (set -a; source settings/shipping.env; set +a; dotnet run config.cs -- env)
Shipping:ApiVersion = 1.10
Shipping:Carriers:0 = standard
Shipping:Carriers:1 = express
Shipping:Cutoff = 12:30
Shipping:DefaultCountry = NO
Shipping:Express = true
Shipping:MaxWeightGrams = 1000
Bound: NO, 12:30, Express=True, 1.10, [standard, express]
Dört format da aynı key'leri üretti; bu yüzden layering çalışıyor: appsettings.json'daki bir değeri bir environment variable ezebiliyor. Ayrıntılar ilginç. JSON provider'ı JSON'daki true'yu True string'i olarak sakladı; configuration'da her değer bir string. YAML provider'ı ise değerleri parse edilmemiş hâliyle, yani NO, 12:30 ve 1.10 olarak bıraktı. Önceki yazılardaki tip tahmini sorunlarının hiçbiri burada yok, çünkü tipe options class'ı karar veriyor. Ama Express: yes'i bool'a çeviremedi; .NET yes'i boolean saymıyor. YAML'ı .NET'te okuyacaksan değerleri .NET'in anlayacağı biçimde yaz. Provider sırası, secret'lar ve ValidateOnStart ASP.NET Core Web API serisinin configuration yazısında ve Configuration ve Secret Management serisinin layering yazısında.
Windows, Linux ve Büyük/Küçük Harf
Windows'ta environment variable adları büyük/küçük harfe duyarsız: Shipping__DefaultCountry ile SHIPPING__DEFAULTCOUNTRY aynı değişken. Linux ve macOS'ta ise iki ayrı değişken. .NET configuration key'leri her platformda duyarsız olduğu için bir Linux container'ında ikisi birden tanımlıysa aynı key'e düşer:
$ Shipping__DefaultCountry=NO SHIPPING__DEFAULTCOUNTRY=SE dotnet run config.cs -- env | grep -i -E "defaultcountry|bound"
Shipping:DefaultCountry = SE
Bound: SE, 00:00, Express=False, , []
Hangisinin kazanacağı tanımlı değil; bu çalıştırmada SE kazandı ve hiçbir uyarı yok. Windows'ta geliştirirken hiç görmediğin bu durum, değişkenleri farklı yazımla ekleyen iki ayrı kaynak (bir Dockerfile ENV'i ve bir Kubernetes manifest'i gibi) olduğunda Linux'ta ortaya çıkar. Ekipçe tek bir yazım seç (Bookstore'da .NET'in property adlarıyla aynı: Shipping__DefaultCountry) ve CI'da tekrar eden adları kontrol et.
Hangi Format Ne Zaman?
| Format | Yorum | Tipler | Şema ve araç desteği | Tipik insan hatası | En uygun yer |
|---|---|---|---|---|---|
| JSON | Yok (JSONC'de var) | Açık, tarih yok | JSON Schema, her dilde parser | Virgül, tırnak | API, makinenin ürettiği config, appsettings.json |
| YAML | # | Tahmin edilen, sürüme bağlı | JSON Schema (editor eklentileriyle), linter | Girinti, tırnaksız değer | Kubernetes, Compose, CI pipeline'ları |
| TOML | # | Açık, tarih ve saat dahil | JSON ve YAML kadar yaygın değil | Uzun iç içe yapıda table başlıkları | Araç ve paket ayarları (pyproject.toml) |
| INI | ;, # (parser'a göre) | Hepsi string | Yok | Satır sonu yorumu, tırnak | Basit, düz ayarlar (.editorconfig) |
| .env | # (parser'a göre) | Hepsi string | Yok | #, $, boşluk | Yerel geliştirme, Compose interpolation |
Bookstore'daki tercihimiz: uygulama ayarları appsettings.json'da (JSONC), environment'a göre değişenler environment variable'da, altyapı YAML'da (Kubernetes, Compose, GitHub Actions), yerel geliştirmede Compose için bir .env. TOML ve INI'yi kendimiz seçmiyoruz ama araçların dosyalarında (.editorconfig, pyproject.toml) okuyabilmemiz gerekiyor. Secret'lar bu formatların hiçbirine girmez: ne appsettings.json'a ne commit'lenen bir .env'e. Geliştirmede user-secrets, production'da bir secret store kullanılır; ayrıntısı Configuration ve Secret Management serisinde.
Üretimde Dikkat
Bir config formatı seçerken asıl soru "dosyayı kim okuyacak?". Tek bir okuyucu varsa (ör. yalnızca .NET), o okuyucunun kuralları yeterli. Aynı dosyayı birden fazla araç okuyorsa (bir .env'i hem Compose hem bir Node.js script'i), en katı ortak alt kümeyle yaz ve bunu CI'da ikisiyle birden test et. Bu yazıdaki # farkı bir takip linkini kısalttı; bir connection string'de aynı fark bir şifreyi kısaltabilir.
Environment variable'ları bir kaynaktan (Kubernetes manifest'i, App Service ayarı, Compose dosyası) toplayıp tek yerde tutmak tekrar eden adları önler. Uygulamanın açılışında bind edilen options'ı doğrulamak (ValidateOnStart) da yanlış formatlanmış bir değerin ilk istekte değil deploy anında fark edilmesini sağlar.
Sık Düşülen Tuzaklar
.env'i bash ile source etmek ve dosyada bash'in anlamadığı bir satır olması; .env'de satır sonuna yorum yazmak; INI'de değerin sonuna ; comment eklemek. Python configparser'ın key'leri küçük harfe çevirdiğini unutmak da sık görülür: c["Shipping"]["TrackingUrl"] çalışır, çünkü arama büyük/küçük harfe duyarsız; ama bölümü dict'e çevirip TrackingUrl key'ini ararsan bulamazsın.
TOML'da en sık hata table başlıklarının kapsamını unutmak: bir [Shipping] başlığından sonra yazdığın her key, bir sonraki başlığa kadar o table'a girer. Dosyanın en üstüne eklemek istediğin bir key'i yanlışlıkla bir table'ın altına yazarsan yeni bir ayar değil, o table'ın bir alanı olur. Son olarak TOML 1.1 özelliklerini (saniyesiz saat, çok satırlı inline table) kullanmadan önce okuyacak araçların 1.1'i desteklediğinden emin ol; Python'un standart tomllib'i 3.14'te henüz desteklemiyor.
Küçük Bir Deneme
★☆☆ sample.env'deki takip URL'sini tek tırnak içine al ve dört aracı tekrar çalıştır. #latest artık her yerde korunuyor mu?
★★☆ settings/shipping.yaml'daki Express: yes'i Express: true yap ve config.cs'i YAML ile tekrar çalıştır. Ardından YAML provider'ından sonra environment variable provider'ını da ekle ve Shipping__Express=false ile YAML'daki değeri ez.
★★★ config.cs'e Tomlyn ile bir TOML kaynağı ekle: TOML dosyasını okuyup Shipping:Rates:0:Region gibi düz key'lere çeviren küçük bir IConfigurationProvider yaz ve diğer dört formatla aynı çıktıyı verdiğini göster.
Bu yazıda formatların ne kadar farklı okunabildiğini gördük. Bir sonraki yazıda bu farkları yazarken yakalamanın yoluna bakacağız: JSON Schema ile editor'de tamamlama ve hata gösterimi, SchemaStore, Bookstore.Api için kendi şemamız ve CI'da doğrulama.