23 Nisan 2020 Perşembe

Type-Safe Configuration (IOptions - IOptionsMonitor - .Net Core)

      S.A. Arkadaşlar,
      Bugün .NET Core ile uğraşanların genelde bildiği ve kullandığı bir konuyu ele alacağız. Bu konu her ne kadar bilinse de, birlikte bir miktar daha ileriye taşımaya çalışalım.
Bazı değişken bilgilerini genellikle .config dosyası içinde tutmaktayız. Bu dosyayı çoğu zaman development ortamı için ayrı, test ortamında ayrı ve production ortamlarında ayrı tutmaktayız. Tabi ki bu örnekler çoğaltılabilir. Asıl aktarmak isteğim mevzu, config değerleri string olarak tutulduğundan dolayı hataya açık bir durum oluşmaktadır zaman zaman. Gelin hep birlikte bunu type-safe bir yapıya taşıyalım.

      Her zaman yaptığımız gibi öncelikle basit olan yoldan başlayacağız. Giderek olması gerekene doğru ilerlemeye özen göstereceğiz. Hangisinin kullanılacağı takdiri sizindir.

     İlk önce akla gelen sabit değerler oluşturup bu değerleri sabit değişkenlere atamaktır. Bunu sabit olarak kod içerisinde tutuyoruz. Bunlar tabi ki genelde değişmeyen verilerdir, fakat sizler de iyi biliyorsunuz ki yazılımda değişmeyen tek şey değişimin kendisidir. Böyle olunca en ufak bir değişiklikte koda versiyon geçmek gerekecektir, fakat biz bunun olmasını istemediğimiz için diğer çözüme geçelim.

     Config dosyası içinde bu verilerimizi tutarsak daha güzel olmaz mı? Bunu farklı ortamlar için de ayrı ayrı tutarsak daha da güzel olacaktır. .Net Core ile birlikte appSettings.Development.json dosyası default olarak gelmektedir. Biz de bunun yanında Production dosyası ekledik. Bunun gibi Test, Preproduction gibi dosyalar da oluşturmak mümkündür. Şimdi örnek config dosyamızı oluşturalım. Daha sonraki konuştuklarımız da somutlaşmış olsun.
{
  //default .net core ile gelir.
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  //OutsideService altında 2 farklı grubumuz mevcuttur.
  //Bunun sebebi gruplamak ve böylece okunurluğu artırmak
  "OutsideService": {
    "Host": {
      "Server": "https://outsideservice.com",
      "Key": "Key",
      "Password": "Password"
    },
    "Constants": {
      "MessageType": "MessageType",
      "Reference": "Reference",
      "Others":  "Others"
    }
  }
}

     Öncelikle nuget üzerinden gerekli paketi indireceğiz. Daha sonra gerekli ayarları yapacağız.
Microsoft.Extensions.Configuration.Abstractions
    Şimdi kullanılacak servisimize onu enjekte ediyoruz.
//entegre edilmesi
private readonly IConfiguration _configuration;
public ExampleService(IConfiguration configuration)
{
     _configuration = configuration;
}
//Kullanımı
_configuration["OutsideService:Host:Key"]

    Bu şekilde daha esnek bir yapıya kavuştuk, fakat burada şöyle bir problem var. Tüm keyleri string olarak tuttuğumuz için her hangi bir hata oluştuğunda bunu fark edemeyebiliriz. Daha sonra başımızın ağrımasına sebebiyet verebilir. Bu yüzden bunu type-safe bir yapıya geçirmek istiyoruz.

    Config dosyası yapımız aynı olacak şekilde yolumuza devam etmek istiyoruz. Bunu birebir karşılayacak bir obje oluşturmak istiyoruz. Daha sonra bunu intellisense özelliği ile de kullanabiliyor olacağız. Şimdi objeyi oluşturalım. Eğer VS kullanıyorsanız bunu kolay bir yolu vardır. JSON içeriğini kopyalayıp yapıştırmak istediğiniz dosyaya Edit/Paste Special/Paste JSON As Classes ile kolaylıkla oluşturabilirsiniz.
public class OutsideService
{
    public Host Host { get; set; }
    public Constants Constants { get; set; }
}

public class Host
{
    public string Server { get; set; }
    public string Key { get; set; }
    public string Password { get; set; }
}

public class Constants
{
    public string MessageType { get; set; }
    public string Reference { get; set; }
    public string Others { get; set; }
}

     Artık objemiz de hazır olduğuna göre şimdi nuget'ten aşağıdaki paketi indirmemiz gerekmektedir.
Microsoft.Extensions.Options
     Şimdi Startup.cs dosyasına gidelim ve gerekli ayarları yapalım.
//Başlangıçta çalışırken oluşturulması
public void ConfigureServices(IServiceCollection services)
{
    //<T> oluşturduğumuz sınıfın ismi iken, 
    //GetSection içindeki key ise json içinde oluşturduğumuz isim
    services.AddOptions();
    services.Configure<OutsideService>(Configuration.GetSection("OutsideService"));
}
//Servis içine entegre edilmesi
private readonly IOptions<OutsideService> _settings;
private OutsideService Settings
{
    get { return _settings.Value; }
}

public ExampleService(IOptions<OutsideService> settings)
{
    _settings = settings;
}
//Kullanımı
OutsideService.Host.Key

    Burada dikkat edilmesi bir şey ise başlangıçta oluşturulan sınıflar singleton oluşturulur ise (sadece bir kerelik oluşturulur) bu yöntemi kullanmanız daha uygun olacaktır. Fakat sınıflar scoped ya da transient oluşturulursa (program çalıştıktan sonra da farklı şekillerde tekrar çağrılabilir) o zaman IOptionsMonitor kullanmanız daha doğru olacaktır. O zaman da Value yerine CurrentValue kullanılması gerekecektir. Çünkü farklı zamanlarda tekrardan oluşturulabileceği için farklı değerler de alabilir. O yüzden şuandaki değeri olan CurrentValue olması gerekmektedir. Zaten IOptionsMonitor yapısı da değişikliği sizin için gözlemektedir ve her hangi bir değişiklikte güncellenmiş değeri kullanmanızı sağlayacaktır.

   Bahsettiğimiz kodu da aşağıda verdikten sonra artık yazımıza son verelebiliriz.
private readonly IOptionsMonitor<OutsideService> _settings;
private OutsideService Settings
{
    get { return _settings.CurrentValue; }
}

public ExampleService(IOptionsMonitor<OutsideService> settings)
{
    _settings = settings;
}

     Bu arada config dosyalarına, connection string user, password vb gibi bilgileri eklerken mutlaka şifrelemeyi (encrpyt etmeyi) unutmayalım.

      Ayrıca şunu da hatırlatmakta faydar var. Veritabanı ortamında tutulan bu bilgiler, uygulamanın ayağa kalkmasıyla IOptions ile birlikte kullanılabilir.

     Şimdi de birim(unit) test ile nasıl kullanıldığına bakalım. Bunun için 2 farklı yöntem var. Aslında IOptions için 2 yöntem varken, IOptionsMonitor için uğraştığım kadarıyla bir yöntem var. O da mock'lama. Aslında buna gerek kalmadan yapabilsek güzel olurdu, ama şuan için çözümünü bulamadım. Şimdi kodlarımıza geçelim.
Options<OutsideService> options = Options.Create(new OutsideService
{
   Constants = new Constants(), //Oluşmasını istediğiniz değerleri buraya girebilirsiniz.
   Host = new Host() //Veya json içersinden desarialize edip almak daha da güzel olur.
});
 var fileContent = File.ReadAllText($@"{yourPath}");
 var outsideService= JsonConvert.DeserializeObject<OutsideService>(fileContent);
 var mockedOptionsMonitor = new Mock<IOptionsMonitor<OutsideService>>();
 mockedOptionsMonitor.Setup(om => om.CurrentValue).Returns(outsideService);

    Yukarıda da belirttiğimiz gibi IOptions için Create methodu var iken, IOptionsMonitor için böyle bir çözüm bulamadım. Ben de çözümü objeyi mock'layarak buldum (istediğim objenin sahtesini oluşturuyorum). Objeye yukarıdaki gibi değer de atayabilirsiniz, fakat ben bunu kod içinde daha kullanılabilir olduğuna inandığım için .json dosyasından alıp kullandım. Şunu da belirtmekte fayda CurrentValue sadece değer okunan olan bir değişkendir (get). O yüzden direkt olarak buna her hangi bir değer atayamıyoruz. Bunu da şu açıdan açıklamakta fayda görüyorum. Mock.Of kullanmak isteyebilirsiniz, fakat CurrentValue'yi set edemeyeceğiniz için problemi çözememiş olabilirsiniz.

     Bu şekilde bir yazımızın daha sonuna geldik. Sürç-i lisan olduysa affola.

     Esnek, okunabilir, hatalara kapalı gelişmeye açık sistemler tasarlamak dileğiyle.

 Kaynak

Hiç yorum yok:

Yorum Gönder