Configuration ve Options pattern: appsettings katmanları ve ayarları koddan ayırmak
Ayarları koddan ayırmayı işliyoruz: appsettings dosyaları ve ortam katmanları, bağlantı dizelerini güvenli yönetmek, Options pattern ile ayarları güçlü tiplerle okumak ve hassas verileri koddan uzak tutmak.
Şimdiye kadar birçok değeri doğrudan koda gömdük: veritabanı bağlantı dizesini, log dosyası yolunu. İşe yaradı ama bu kötü bir alışkanlık. Çünkü bu değerler ortama göre değişir; geliştirme makinende bir veritabanı, canlı sunucuda bambaşka bir veritabanı kullanırsın. Her değişiklikte kodu düzenleyip yeniden derlemek istemezsin. Bugün ayarları koddan nasıl ayıracağımızı ve .NET'in bunu ne kadar zarif çözdüğünü konuşuyoruz.
Neden Ayarları Koddan Ayırıyoruz?
Basit bir prensiple başlayalım: değişen şeyler ile mantık birbirinden ayrı durmalı. Kodun kendisi, ne yapılacağını tarif eder ve nadiren değişir. Ayarlar ise nerede, hangi değerlerle çalışılacağını söyler ve sık sık değişir. Bu ikisini karıştırdığında, küçük bir değer değişikliği için bile koda dokunmak zorunda kalırsın.
Bunu bir cihazın kullanım kılavuzu ile ayar düğmeleri gibi düşün. Kılavuz (kod) cihazın nasıl çalıştığını anlatır ve sabittir. Düğmeler (ayarlar) ise kullanıcının ihtiyacına göre çevrilir. Kimse ses seviyesini değiştirmek için cihazın devre şemasını yeniden çizmez; sadece düğmeyi çevirir. İyi bir uygulama da böyle olmalı: davranışını değiştirmek için kodu değil, ayarları düzenlersin.
appsettings.json: Ayarların Evi
.NET projelerinde ayarların doğal evi appsettings.json dosyasıdır. İkinci yazıda proje kurulurken bu dosyanın oluştuğunu görmüştük. İçine, uygulamanın ihtiyaç duyduğu ayarları düzenli bir yapıda koyarsın. Örneğin veritabanı bağlantı dizesini ve önceki yazıdaki log ayarını buraya taşıyalım:
{
"ConnectionStrings": {
"Default": "Data Source=app.db"
},
"Logging": {
"FilePath": "logs/app.txt"
}
}
Artık bu değerler koda gömülü değil, dışarıda, düzenlenebilir bir dosyada. Bağlantı dizesini değiştirmen gerektiğinde kodu derlemeye gerek yok; sadece bu dosyayı güncellersin. Peki bu değerleri kodun içinden nasıl okuyacağız?
Ayarları Okumanın Basit Yolu
.NET, bu dosyayı otomatik olarak okur ve içeriğini IConfiguration adında bir yapı üzerinden sana sunar. Program.cs içinde bağlantı dizesini şöyle çekebilirsin:
var connectionString = builder.Configuration
.GetConnectionString("Default");
builder.Services.AddDbContext(options =>
options.UseSqlite(connectionString));
Dokuzuncu yazıda bu bağlantı dizesini doğrudan koda yazmıştık; şimdi onu dosyadan okuyoruz. GetConnectionString("Default"), appsettings.json'daki ConnectionStrings bölümünün altındaki Default değerini getirir. Kod artık hangi veritabanına bağlanacağını bilmiyor; bunu ayar dosyasına sormuş oluyor.
Ortam Katmanları: Development ve Production
İşte .NET'in en zarif özelliklerinden biri. Geliştirme yaparken kullandığın ayarlar ile canlıdaki ayarlar farklıdır. .NET bunu, katmanlı ayar dosyalarıyla çözer. Ana appsettings.json'ın yanında, ortama özel dosyalar tutabilirsin:
appsettings.json // Tüm ortamlar için temel ayarlar
appsettings.Development.json // Sadece geliştirme ortamı
appsettings.Production.json // Sadece canlı ortam
Mantık şöyle işler: önce temel dosya okunur, sonra üzerine o anki ortamın dosyası eklenir ve çakışan değerler ortama özel olanla değiştirilir. Yani geliştirme ortamında çalışırken temel ayarlar geçerlidir ama appsettings.Development.json'daki değerler onları ezer. Böylece "geliştirmede yerel veritabanı, canlıda gerçek veritabanı" gibi ayrımları tek satır kod yazmadan yapabilirsin. Uygulama, hangi ortamda çalıştığını bir ortam değişkeninden anlar ve doğru dosyayı kendisi seçer.
Options Pattern: Ayarları Güçlü Tiplerle Okumak
IConfiguration'dan değerleri tek tek, isimle çekmek küçük ihtiyaçlar için iyi. Ama ilişkili birden çok ayarın olduğunda, bunları her seferinde string anahtarlarla okumak hem hataya açık hem de dağınık olur. İşte Options pattern burada devreye giriyor: ilgili ayarları bir C# sınıfına bağlarsın ve onlara güvenli, güçlü tiplerle erişirsin.
Diyelim ki loglama için birkaç ayarımız var. Önce bunları temsil eden sade bir sınıf yazarız:
public class LoggingOptions
{
public string FilePath { get; set; }
public int RetentionDays { get; set; }
}
Sonra appsettings.json'daki ilgili bölümü bu sınıfa bağlarız:
builder.Services.Configure(
builder.Configuration.GetSection("Logging"));
Bu satır, appsettings.json'daki Logging bölümünü alıp LoggingOptions sınıfının alanlarına yerleştirir; tıpkı yedinci yazıdaki model binding gibi, isimler eşleştikçe otomatik doldurur. Artık bir servisin içinde bu ayarlara doğrudan, tip güvenli biçimde ulaşabilirsin:
public class LogArchiver
{
private readonly LoggingOptions _options;
public LogArchiver(IOptions options)
{
_options = options.Value;
}
}
Constructor'da IOptions<LoggingOptions> isteriz, .Value ile de asıl ayar nesnesine ulaşırız. Artık _options.FilePath yazdığında, editörün sana otomatik tamamlama verir ve yanlış bir anahtar adı yazma riskin ortadan kalkar. String anahtarlarla boğuşmak yerine, ayarlar artık kodun bir parçası gibi güvenli.
Hassas Verileri Nereye Koymalı?
Kritik bir uyarı: şifreler, API anahtarları, gerçek veritabanı parolaları gibi hassas veriler appsettings.json'a yazılmamalı. Çünkü bu dosya genelde kod deposuna (Git gibi) dahil edilir ve oraya koyduğun bir sır, tüm ekiple ve bazen tüm dünyayla paylaşılmış olur. Geliştirme ortamında bunun için .NET'in User Secrets özelliği vardır; sırları projenin dışında, senin makinene özel bir yerde tutar. Canlı ortamda ise ortam değişkenleri ya da özel bir sır yönetim servisi kullanılır. Kural basit: sırlar asla kod deposuna girmemeli.
Küçük Bir Deneme
Dokuzuncu yazıda koda gömdüğün veritabanı bağlantı dizesini appsettings.json'a taşı ve Program.cs'i bu değeri dosyadan okuyacak şekilde güncelle. Sonra bir appsettings.Development.json dosyası oluştur, içine farklı bir bağlantı dizesi koy ve uygulamayı geliştirme ortamında çalıştırıp hangisinin geçerli olduğunu gözlemle. İstersen bir adım daha atıp küçük bir ayar grubunu Options pattern ile bir sınıfa bağla. Bu deney, ayarların koddan ayrılmasının ne kadar rahatlatıcı olduğunu bizzat gösterir.
Bir sonraki yazıda async/await konusuna gireceğiz: bu seride kod örneklerinde sıkça gördüğün async ve await anahtar kelimelerinin gerçekte ne yaptığını, bir API'de neden bu kadar önemli olduklarını ve doğru asenkron kullanımın performansa etkisini konuşacağız. Şimdiye kadar hep kullandık; artık nasıl çalıştığını anlama zamanı.