.Net Core İle Arkaplan Servisleri

Faruk Terzioğlu
8 min readSep 1, 2019

Uygulama geliştirirken sık sık ihtiyacımız olan ve sürekli farklı varyasyonlarını geliştirmemiz gereken bir uygulama türü, arka planda çalışmasını istediğimiz servisler. Örneğin, sürekli olarak bir kuyruğa mesaj yazılması veya kuyruktan mesaj okunması, cache’in belli aralıklarla kırılması, başka sistemlerin event’lerinin dinlenmesi gibi.

Bunları windows servis, zamanlanmış görevler şeklinde veya bir konsol uygulamasında sonsuz döngüler şeklinde tasarlayabiliriz.

.Net Core ile bu tarz uygulamalara bir standart getirilmiş ve bu sayede arka planda sürekli çalışmasını istediğiniz veya belli zamanlarda tetiklenmesini istediğimiz işleri bir yapı altına toplamışlar; IHostedService ve GenericHost;

Bu yazı içerisinde GenericHost kullanarak BackgroundService’ler (IHostedService) çalıştırırken ihtiyaç duyabileceğiniz noktalara değineceğim. Arka planda çalışmasını istediğiniz işleriniz için yeni bir uygulama geliştirmek istediğinizde burada değindiğim konuları referans olarak alabilir, Github’daki projeden yeni uygulamanızı üretebilirsiniz.

Yazı içerisindeki kod örnekleri Github Gist’i olarak paylaşıldığı için mobil uygulamada büyük fontta gözükebilir, mobil tarayıcılarda ise hiç görüntülenmeyebilir. En iyi sonucu normal bir tarayıcıda alabilirsiniz.

Background Service

Arka planda işletilmesini istediğimiz işleri bir ‘BackgroundService’ sınıfı altında tanımlayacağız. ‘BackgroundService’in başlatılması ve sonlandırılması ‘GenericHost’ tarafından kontrol edilmekte. Bir ‘background service’ ve ‘generic host’ oluşturmak üzere yeni bir konsol uygulaması oluşturalım ve gerekli paketleri ekleyelim;

$ dotnet new console -n ConsoleApp
$ cd ConsoleApp
$ dotnet add package Microsoft.Extensions.Hosting

Program.cs dosyasının içeriğini aşağıdaki gibi düzenleyelim;

Yukarıdaki kodda yaptığımız, HostBuilder aracılığı ile yeni bir ‘Generic Host’ oluşturuyoruz. ‘ConfigureServices’ metodu içerisinde uygulama içerisinde kullanacağımız sınıfları dahili IoC Container’a kaydedeceğiz.

Host tarafından otomatik olarak çalıştırılacak servisleri ‘.AddHostedService’ metodu ile IoC Container’a ekliyoruz.

Son olarak da ‘builder.RunConsoleAsync’ ile host’u çalıştırıyoruz. ‘RunConsoleAsync’ metodu, uygulamaya konsol desteği eklediği için uygulamayı durdurmak üzere Ctrl+C komutunu veya SIGTERM sinyalini beklenir.

Arka planda çalışacak servisi de şu şekilde tanımlayalım;

‘ApplicationService’ servisini ‘BackgroundService’ sınıfından türeterek otomatik olarak çalıştırılacak bir arka plan servisi tanımlıyoruz. ‘ExecuteAsync’ metodu içerisine de çalıştırılmasını istediğimiz kodu yazıyoruz. Yukarıdaki örnek, uygulama durana kadar 10 saniyede bir konsola ‘Hello World’ yazacaktır.

ExecuteAsync metodunun parametresi olan ‘stoppingToken’ ı kendi kodunuzda ‘CancellationToken’ kabul eden her yere (API çağrıları, thread sleep gibi) ileterek uygulama durdurulduğu zaman, vakit alan işlemlerin beklenmeden sonlandırılmasını sağlayabilirsiniz.

Buraya kadar olan kısımda basit bir arka plan servisi tanımladık ve uygulama çalışır çalışmaz ‘ExecuteAsync’ içerisindeki kodların çalışmasını sağladık. Konsol uygulamasını çalıştırarak çıktıları görebilirsiniz.

Dependency Injection

İkinci olarak dahili IoC container’a ikinci bir sınıf (Repository) ekleyeceğiz ve tanımladığımız arka plan servisinde kullanacağız. Eklenen satır;

services.AddTransient<IRepository, Repository>();

Yeni kaydettiğimiz sınıfı arka plan servis içerisinde kullanalım;

‘.ConfigureServices()’ metodu içerisinde, uygulamada kullanılacak tüm sınıfları eklemeliyiz. Bunları ‘singleton’, ‘transient’ veya ‘scoped’ olarak ekleyebilirsiniz. Detayları için şuraya bakabilirsiniz;
https://docs.microsoft.com/tr-tr/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.2#service-lifetimes

Configuration

Uygulama içerisinde kullanılacak çeşitli ayarları appsettings dosyasından, ortam değişkenlerinden, komut satırından veya harici bir sistemden alabilirsiniz. Ayarları appsettings.json dosyasından almak için gerekli olan paketleri ekleyelim;

$ dotnet add package Microsoft.Extensions.Configuration
$ dotnet add package Microsoft.Extensions.Configuration.Json
$ dotnet add package Microsoft.Extensions.Options.ConfigurationExtensions

İlk olarak ‘appsettings.json’ dosyasından okumak için gerekli kodlardan bahsedeyim. Appsettings dosyasını uygulamaya dahil etmek üzere generic host builder’a aşağıda olduğu gibi ‘.ConfigureAppConfiguration()’ metodunu ekleyelim;

‘ConfigureAppConfiguration’ metodu içerisinde uygulama ayarlarını nereden okuyacağımıza dair ayarları yapıyoruz. ‘.SetBasePath()’ ile ayarlar dosyasını nerede bulacağımızı belirtiyoruz. ‘.AddJsonFile()’ ile de kullanmak istediğimiz appsettings dosyasını belirtiyoruz. Eğer appsetting dosyasının kesinlikle okunmasını istiyorsanız, ‘optional’ parametresini false yapabilirsiniz. Bu sayede uygulama başladığı sırada bu dosya bulunamazsa uygulama hata verecektir.

Örnek olarak aşağıdaki gibi bir ayarlar dosyası oluşturalım;

Bu ayarları temsilen aşağıdaki sınıfları oluşturalım;

Appsettings dosyasındaki değerleri C# sınıflarına aktarmak üzere aşağıdaki satırları ekleyelim;

‘ConfigureServices’ metodu içerisinde appsettings’deki değerler ile doldurulmasını istediğini sınıfları ‘.Configure()’ ile ayarlıyoruz. ‘ApplicationSettings’ sınıfını appsettings içerisindeki değerler ile dolduracağımız için metoda parametre olarak ‘hostContext.Configuration’ değerini geçtik. ‘AuthenticationSettings’ sınıfını ise bir alt seviyedeki ‘authentication’ değerleri ile dolduracağımız için appsettings içerisindeki bir bölümü seçmek üzere parametre olarak ‘hostContext.Configuration.GetSection(“authentication”)’ değerini gönderdik.

Artık bu ayarlara, daha önce IoC container’a eklediğimiz sınıflar içerisinden erişebiliriz;

Constructor parametresi olarak IOptions<> tipinde bir parametre tanımlamalıyız. Bu parametrenin ‘.Value’ değeri üzerinden istediğimiz appsettings değerlerine erişebiliriz. Başka bir örneği de;

Development, Test, Production Ortamları

Eğer uygulamanız prod, test veya development gibi farklı ortamlarda çalışıyor ve her ortam için farklı ayar dosyaları tutmak istiyorsanız bunun için de aşağıdaki değişiklikleri yapmalıyız. İlk olarak uygulamanın çalıştığı ortam bilgisini almalıyız. Bunun için “hostsettings.json” isminde bir dosya oluşturup içerisine ‘“environment”: “Development”’ şeklinde bir değer girmeliyiz. Farklı ortamlar için farklı “environment” değerleri tanımlayabilirsiniz.

Uygulamanın çalıştığı ‘host’ ile ilgili ayarları (environment gibi) eklemek üzere ‘.ConfigureHostConfiguration()’ metodunu çağırıyoruz. Bu metot içerisinde, daha önce appsettings.json için yaptığımız gibi dosyayı nerede bulacağımızı ve dosyanın ismini belirtiyoruz.

Eğer “ortam” bilgisini (test/prod vs.) bir dosya yerine makinenin ortam değişkenlerinden (environment variables) almak istiyorsak ayriyeten ‘.AddEnvironmentVariables()’ metodunu da çağırmalıyız. Bunun için gerekli paketi eklemek üzere;

$ dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables

Fark ettiyseniz ‘AddEnvironmentVariables’ metoduna parametre olarak ‘CONSOLEAPP_’ şeklinde bir prefix tanımladık. Bu sayede bir makine içerisinde birden fazla uygulamanın ayarlarını tutabiliriz.

Örnek olarak, debug yaparken ortam bilgisini (development) almak üzere Visual Studio Code için ‘.vscode/launch.json’ dosyasını aşağıdaki gibi değiştirebiliriz. Benzer şekilde Visual Studio’nun debug ayarlarını da düzenleyebilirsiniz.

‘hostsettings.json’ dosyasındaki ‘environment’ değişkeni yerine, daha önce tanımladığımız prefix ile ‘CONSOLEAPP_ENVIRONMENT’ şeklinde bir değer tanımlıyoruz. Prefix şart değil, ama aynı makinede çalışan uygulamaları ayırt etmek için kullanılabilir. ‘environment variables’ altında tanımlanan değerler ‘hostsettings.json’ dosyasındaki değerleri ezecektir.

debug’a alternatif olarak uygulamanızı ‘dotnet run’ komutu ile çalıştırırken farklı ortam değişkenlerini test etmek isterseniz de, o anki terminal oturumuna aşağıdaki gibi ortam değişkeni ekleyebilirsiniz;

$ export CONSOLEAPP_ENVIRONMENT=Development

Farklı ‘environment’ değerleri ile uygulamanın çalışmasını aşağıda görebilirsiniz. ‘Hosting environment: … ’ kısmına dikkat edin;

Uygulamanın çalışacağı ortam bilgisini aldığımıza göre artık bu ortamlara göre farklı ayar dosyaları oluşturup uygulamada kullanabiliriz. Geliştirme ortamı için ‘appsettings.Development.json’ ve test ortamı için de ‘appsettings.Test.json’ dosyalarını oluşturalım.

Bu dosyalar içerisine sadece ortama göre değişmesini istediğiniz değerleri yazıyoruz. Buraya yazılan değerler appsettings.json dosyasındakileri ezecektir. Burada belirtilmeyen tüm değerler ise appsettings.json dosyasından okunacak. Ortam bilgisine göre ilgili ayarlar dosyasının okunması için ‘.ConfigureAppConfiguration()’ metoduna aşağıdaki satırı ekleyelim;

configApp.AddJsonFile($”appsettings.{hostContext.HostingEnvironment.EnvironmentName}.json”, optional: true);

Uygulamanız ile ilgili ayarları dosyalar yerine işletim sisteminin ortam değişkenlerinden almak isterseniz, daha önce host ayarlarında yaptığımız gibi bir ayar yapmamız gerekiyor. Bunun için Program.cs de aşağıdaki değişiklikleri yapalım. Ortam değişkenlerinden verilen değerler daha önce ‘appsettings.json’ veya ‘appsettings.Development.json’ dosyalarında tanımlanan değerleri ezecektir.

Örnek olarak bazı değerleri ortam değişkeni olarak tanımlamak istersek, VSCode debug dosyasını şu şekilde güncelleyebiliriz;

"intervalMs" : "500" değeri appsettings.json dosyasında tanımlanan değeri ezecektir. Eğer sadece ortam değişkenlerinden okumak istediğiniz değerler varsa, appsettings dosyalarına yazmayabilirsiniz.

Farkettiyseniz “authentication__username" ve authentication__password değerleri iki alt tire ile ayrılmış iki kelimeden oluşuyor. Ayarlar sınıfındaki bir alt seviyedeki değerlere (AuthenticationSettings.Username gibi) ulaşmak üzere her bir seviye için kelimelerin aralarına iki alt tire koymak gerekiyor.

‘appsettings.json’ içerisinden uygulama ayarları olarak bir liste almak istediğimizde bunu basit bir json liste olarak tanımlayabiliriz. Bunun yerine listeyi ortam değişkenlerinden almak istediğimizde ise şu şekilde tanımlamamız gerekmekte;

Örnek olarak tanımladığım ‘List<string> AvailableServers’ listesinin her bir elemanı için ortam değişkenlerine ‘availableServers:0’ şeklinde değerler girmemiz gerekiyor.

Uygulama ayarlarını harici bir sistemden (Consul gibi) almak istersek de aşağıdaki gibi ‘.Configure()’ metodu içerisinde tanımlayabiliriz;

Eğer IOption<> yöntemini kullanmak istemiyor veya geliştirdiğiniz kütüphaneler ayarlar olarak POCO sınıflar alıyorlarsa, appsettings.json’da veya ortam değişkenlerinde tanımladığımız değerleri POCO sınıflara atamak için aşağıdaki değişikliği yapabiliriz;

Program.cs içerisinde, ilgili sınıfdan (AuthenticationSettings) bir instance alıyoruz ve ‘.Configuration.Bind()’ ile config dosyasındaki ilgili bölümü (authentication gib) okuyoruz. Daha sonra da, tüm alanları, ilgili uygulama ayarları ile doldurulmuş POCO sınıfını IoC container’a ekliyoruz. Bu sınıfı daha sonra constructor’dan alarak istediğiniz yerde (Repository gibi) kullanabilirsiniz.

Logging

Herhangi bir harici log kütüphanesine gerek kalmadan dahili log mekanizmasını kullanmak için aşağıdaki paketleri ekleyelim;

$ dotnet add package Microsoft.Extensions.Logging
$ dotnet add package Microsoft.Extensions.Logging.Configuration
$ dotnet add package Microsoft.Extensions.Logging.Console

Generic host üzerinden logger’ı aktifleştirmek için ‘ConfigureLogging’ metodunu aşağıdaki gibi ekleyelim;

Burada yaptığımız; ‘ClearProviders’ ile mevcut tüm log sağlayıcılarını (konsol vs) kaldırıyoruz. AddConfiguration ile log ayarlarını nereden alacağımızı tanımlıyoruz. Daha önce ‘AuthenticationSettings’ için yaptığımız gibi appsettings dosyasındaki bir bölüm içerisinden (Logging) alacağız. Logların konsola yazılmasını istediğimiz için ‘logging.AddConsole()’ satırını ekliyoruz. Konsol gibi, dahili diğer log sağlayıcıları; Event log, event source, debug, trace ve Azure services.

Log ayarları olarak şunları tanımlayabiliriz;

‘Logging.LogLevel’ altındaki ayarlar ile minimum hangi seviyedeki mesajları loglamak istediğimizi belirliyoruz. Bunlar küçükten büyüğe Trace, Debug, Information, Warning, Error ve Critical. Örneğin buradaki değeri Information olarak tanımlarsak Debug ve Trace mesajları kaydedilmeyecek. Yukarıdaki ayarda olduğu gibi sistem mesajları ve Microsoft mesajları için farklı seviyeler tanımlayabilirsiniz. appsettings’deki ‘Logging’ ayarları içerisinde farklı log’layıcıları için farklı seviyeler tanımlayabiliriz. Yukarıdaki örnekte, konsola ‘Information’ seviyesinde yazarken event log’a ‘Warning’ seviyesinde yazabiliriz.

Log mekanizmasını aktifleştirip ayarlarını yaptıktan sonra, kendi sınıflarımız içerisinden logger’a aşağıdaki gibi erişebiliriz;

Farklı seviyelerde loglar yazmak için ‘_logger.LogInformation’ veya ‘_logger.LogDebug’ gibi metotları kullanabiliriz. Buradan gönderilen log mesajları appsettings’deki ayarlara göre ilgili loglayıcılara gönderilir.

Farklı ortamlar için farklı log ayarları tanımlamak isterseniz ilgili
‘appsettings.{EnvironmentName}.json’ dosyası içerisinde log ayarlarını ezebilirsiniz;

Log ile ilgili detaylar için;

Varsayılan logger yerine Serilog kullanmak istersek ilk olarak aşağıdaki paketleri kuralım;

$ dotnet add package Serilog.Extensions.Hosting
$ dotnet add package Serilog.Sinks.Console
$ dotnet add package Serilog.Settings.Configuration

Varsayılan logger’ı kaldırmak için ‘.ConfigureLogging()’ metodunu tamamen silelim ve onun yerine aşağıdaki metodu çağıralım;

Serilog ile ilgili ayarları appsettings.json içinde şu şekilde tanımlayalım;

Settings__RestServer__Liquid__RestUrl

Bu ayarlar sayesinde IoC container’dan aldığımız ILogger<> ile gönderdiğimiz log mesajları Serilog’a gidecek ve buradan da yukarıdaki (appsettings’deki) ayarlara göre konsola ve Loggly sitesine gidecek.

Servisler Arası İletişim

Bir generic host içerisinden birden fazla arka plan servisi eş zamanlı olarak çalıştırabiliriz. Örnek olarak ‘ApplicationService’ den bir tane fakat ‘AnotherService’ servisinden iki tane çalıştırmak istersek aynı arka plan servisini iki kere kaydedebiliriz;

Eş zamanlı çalışan arka plan servisleri arasında veri alışverişi yapmak istediğimizde thread-safe bir çözüm bulmalıyız. Bunun için tercih ettiğim bir yöntem, bir kuyruk tanımlayarak bir veya birden fazla arka plan servisinin bu kuyruğa yazmasını ve bir veya birden fazla servisin de bu kuyruktan okumasını sağlamak. Örnek olarak bir ‘ProducerService’ arka plan servisinden gönderilen mesajları iki adet ‘ConsumerService’ in tüketmesini istersek bunları aşağıdaki gibi tanımlayabiliriz;

Servisler arası veri iletişimi için thread-safe olan ConcurrentQueue veri tipini kullanıyorum. Bu kuyruk nesnesini IoC container’a ekleyerek istenilen yerden erişilmesini sağlıyoruz. ‘Producer’ servisinden kuyruğa mesaj göndermek için;

ve bu kuyruktan mesaj okumak için de;

şeklinde yeni servislerimizi oluşturabiliriz.

ConsumerService’i iki kere eklediğimiz için aynı kuyruğu iki farklı servis tüketecek ve bu sayede kuyrukta mesaj birikmesinin önüne geçilecek. İhtiyacınıza göre arka plan servislerinin sayısını artırabilirsiniz.

Servislerin Durdurulması

Arka plan servislerini tasarlarken dikkat etmemiz gereken bir konu da herhangi bir hata aldığında uygulamayı veya servisleri nasıl düzgünce (gracefully) durduracağımız.

Arka plan servislerimizi, bir soyut sınıf olan BackgroundService sınıfından türetiyoruz. BackgroundService ise aslında ‘IHostedService’ arayüzünü implement eden bir arka plan servisi. Başka kaynaklarda generic host ile arka plan uygulama örneklerinde direkt olarak IHostedService kullanıldığını görebilirsiniz. BackgroundService sınıfı, bazı detayları gizleyerek bizlere kolaylık sağlamış.

IHostedService arayüzünü incelerseniz ‘Task StopAsync()’ şeklinde bir metot daha tanımlıyor. ‘BackgroundService’ sınıfı da bu metodu ezmemize olanak veriyor. Bu metot içerisinde, servis durduğu takdirde yapmamız gereken işlemleri (dispose vs.) yapabilmemizi sağlıyor. Bu sayede servislerimizi ve uygulamamızı düzgün bir şekilde durdurabiliriz.

Örnek olarak uygulama için kritik olan ve hata aldığı takdirde uygulamayı da durdurmamız gereken ‘ApplicationService’ servisinde bir hata olduğunda, bunu yakalayıp logladıktan sonra uygulamayı durdurmak için kodumuzu şu şekilde değiştirmeliyiz;

Burada, constructor’dan aldığımız ‘IApplicationLifetime’ üzerinden uygulamanın durdurulmasını sağlayabiliriz. Bu sayede diğer servislere de durdurma komutu gidecek ve onlar da uygun şekilde duracaktır.

Uygulamanın devamlılığı için kritik olmayan ama yine de hata aldığı taktirde servisi durdurmadan önce bazı işlemler yapmamız gereken ‘AnotherService’ servisinde de aşağıdaki eklemeleri yapmalıyız;

Burada yaptığımız ise, eğer servis çalışma sırasında bir hata alırsa bunu catch bloğu içerisinde logluyor ve servisin ‘StopAsync()’ metodunu çağırıyoruz. Bu metotta da servislerimizin düzgün bir şekilde durdurulması için gerekli olan işlemleri gerçekleştiriyoruz.

.Net Core ile arka planda çalışmasını istediğimiz işler için bir uygulama geliştirdiğimizde, sıklıkla ihtiyaç duyabileceğimiz yöntemlere değindim. Eğer ek olarak sizin de kullandığınız yöntemler varsa yorumlarda belirtebilirsiniz.

https://www.linkedin.com/in/farukterzioglu

https://twitter.com/farukterzioglu

https://github.com/farukterzioglu

--

--