.NET Core ile Worker Service Oluşturma

.NET Core 3.0 ile beraber Worker isminde yeni bir proje şablonu geldi. Bu proje şablonu sayesinde Windows ve Linux servisleri oluşturabiliyoruz.

Konuyu daha da pekiştirebilmek için örnek bir uygulama üzerinden devam edelim. Yapacağımız bu uygulama saniyede bir bulunduğu zamanı log olarak yazacaktır.

Şimdi aşağıdaki komutu terminale yazarak yeni bir worker projesi oluşturalım.

$ dotnet new worker -o aspnetcore-worker-sample -n AspNetCore.WorkerSample
$ aspnetcore-worker-sample

Projemizi kullanmış olduğumuz IDE veya metin editörüyle açalım. Proje şablonumuz aşağıdaki benzer yapıda oluşmaktadır.

Program.cs dosyamızın yapısı aşağıdaki gibidir.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace AspNetCore.WorkerSample
{
	public class Program
	{
		public static void Main(string[] args)
		{
			CreateHostBuilder(args).Build().Run();
		}

		public static IHostBuilder CreateHostBuilder(string[] args) =>
			Host.CreateDefaultBuilder(args)
				.ConfigureServices((hostContext, services) =>
				{
					services.AddHostedService();
				});
	}
}
  1. satırda görüleceği üzere .NET Core 2.1 ile birlikte gelen Background Task'lardan faydalanılmaktadır. Varsayılan olarak Worker ismindeki bir sınıfın Baackground Task olarak kullanılacağı belirtilmiştir.

Şimdi ise Worker.cs dosyamızın içeriğini inceleyelim. Dosyamızın içeriği aşağıdaki gibidir.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace AspNetCore.WorkerSample
{
	public class Worker : BackgroundService
	{
		private readonly ILogger _logger;
		private Task _executingTask;
		private CancellationTokenSource _cts;

		public Worker(ILogger logger)
		{
			_logger = logger;
		}

		public override Task StartAsync(CancellationToken cancellationToken)
		{
			_logger.LogWarning("Worker service started.");

			_cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
			_executingTask = ExecuteAsync(_cts.Token);

			return _executingTask.IsCompleted ? _executingTask : Task.CompletedTask;
		}

		public override Task StopAsync(CancellationToken cancellationToken)
		{
			if (_executingTask == null)
			{
				return Task.CompletedTask;
			}

			_logger.LogWarning("Worker service stopping.");

			_cts.Cancel();

			Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken)).ConfigureAwait(true);

			cancellationToken.ThrowIfCancellationRequested();

			_logger.LogWarning("Worker service stopped.");

			return Task.CompletedTask;
		}

		protected override async Task ExecuteAsync(CancellationToken stoppingToken)
		{
			while (!stoppingToken.IsCancellationRequested)
			{
				_logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
				await Task.Delay(1000, stoppingToken);
			}
		}
	}
}

Worker sınıfı BackgroundService ismindeki bir abstract sınıftan türetilmiştir. Bu sınıfın içerisinde ExecuteAsync, StartAsync ve StopAsync isminde olmak üzere 3 adet ezilebilir metot bulunmaktadır.

StartAsync metodunun içerisine servis başlarken yapılması geren işlerin yazılması gerekmektedir ve bu metot içerisinde ExecuteAsync metodu çalıştırılmalıdır. ExecuteAsync metodu çağırılırken StartAsync'e ait CancellationToken'ın parametre olarak gönderilmesi gerekmektedir.

ExecuteAsync metodunun içerisinde ise long-running çalışması gereken kodlarımızın bulunması gerekmektedir.

StopAsync metodunda ise uygulamamız veya servisimiz kapanırken yapılması gereken kodların yazılması gerekmektedir. Örneğin kullanılan resource'ların temizlenmesi, açılan veritabanı bağlantıları veya dosyaların kapatılması gibi işlemler bu metot içerisinde yapılmalıdır.

StartAsync ve StopAsync metotlarını kullanmak zorunlu değildir. Bu metotlar kullanılmadığı zaman servis çalışmaya başlamak için ExecuteAsync metodunu çağıracaktır.

Uygulamamızı çalıştırıp durdurduğumuzda aşağıdaki ekran görüntüsüne benzer bir ekranla karşılaşacağız.

Gördüğünüz gibi servisimiz çalışır çalışmaz ilk önce StartAsync metodunda ekrana yazdırdığımız Worker service started. log satırını görmekteyiz.

Akabinde ise ExecuteAsync metodunun ekrana o anın tarih ve saatini yazdırdığı log satırlarının devam ettiğini görüyoruz.

En sonunda ise StopAsync metodunun çalıştığını ve ekrana sırayla Worker service stopping. ve Worker service stopped. log satırlarını yazdığını gördük.

Şimdi ise yazmış olduğumuz worker service'i nasıl Windows servisi veya Linux daemon'ı olarak kullanabileceğimize bakalım.

Bu işlem için UseWindowsService ve UseSystemd metotlarından yararlanacağız.

Öncelikle aşağıdaki komutla ilgili paketleri projemize ekleyelim.

$ dotnet add package Microsoft.Extensions.Hosting
$ dotnet add package Microsoft.Extensions.Hosting.WindowsServices
$ dotnet add package Microsoft.Extensions.Hosting.Systemd

İlgili paketleri projemize ekledikten sonra Program.cs dosyamızı aşağıdaki şekilde düzenliyoruz.

Windows Servisleri:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace AspNetCore.WorkerSample
{
	public class Program
	{
		public static void Main(string[] args)
		{
			CreateHostBuilder(args).Build().Run();
		}

		public static IHostBuilder CreateHostBuilder(string[] args) =>
			Host.CreateDefaultBuilder(args)
				.UseWindowsService()
				.ConfigureServices((hostContext, services) =>
				{
					services.AddHostedService();
				});
	}
}

Linux Daemonları:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace AspNetCore.WorkerSample
{
	public class Program
	{
		public static void Main(string[] args)
		{
			CreateHostBuilder(args).Build().Run();
		}

		public static IHostBuilder CreateHostBuilder(string[] args) =>
			Host.CreateDefaultBuilder(args)
				.UseSystemd()
				.ConfigureServices((hostContext, services) =>
				{
					services.AddHostedService();
				});
	}
}

Windows ve Linux servisleri arasındaki tek fark kullanmış olduğumuz extension metotların farklı olmasıdır. Windows servisleri için UseWindowsService, Linux daemonları için ise UseSystemd extension metodunu kullanıyoruz.

Şimdi ise sırada servisimizi ilgili işletim sistemlerine yüklemeye. Bu işlem için öncelikle worker servisimizi aşağıdaki komut yardımı ile publish alıyoruz.

$ dotnet publish -o publish

Publish aldıktan sonra servisimizi kurmaya başlayabiliriz.

Linux Ortamına Servisin Kurulması

Linux işletim sistemlerinde servisler birer unit tanımına sahiptir. Bizim servisimiz için ilgili unit tanımı aşağıdakine benzer olacaktır.

[Unit]
Description=Sample Worker Service

[Service]
Type=notify
ExecStart=/Users/mennankose/Projects/aspnetcore-worker-sample/publish/AspNetCore.WorkerSample

[Install]
WantedBy=multi-user.target

Yazmış olduğumuz bu unit dosyasını /etc/systemd/system/ klasörünün altına kaydedeceğiz. Ben /etc/systemd/system/workersample.service olarak kaydettim.

Sonrasında ise aşağıdaki komutu kullanarak daemon listesinin yenilenmesini sağlıyoruz.

$ sudo systemctl daemon-reload

Unit dosyasında herhangi bir sorun yoksa servisimiz başarılı bir şekilde kurulmuş olacaktır. Servisimizin çalışma durumunu kontrol etmek için aşağıdaki komutu kullanabiliriz.

$ sudo systemctl status workersample

Windows Ortamına Servisin Kurulması

Windows ortamına servisimizi kurmak için sc.exe aracından yararlanıyoruz. PowerShell'i veya Komut İstemi'ni yönetici olarak çalıştırarak aşağıdaki komutu çalıştırıyoruz.

C:\Windows\System32\sc create WorkerSample binPath=C:\projects\aspnetcore-worker-sample\publish\AspNetCore.WorkerSample.exe

Bu komut sayesinde servisimiz Windows işletim sistemine yüklenmiş olacaktır. İlgili servisin durumunu services.msc üzerinden kontrol edebilirsiniz.

İlgili örneğe https://github.com/mennan/aspnetcore-worker-sample adresinden erişebilirsiniz.