Ana içeriğe geç

Hangfire ve Quartz.NET'i Değiştirmeden İzlemek

· 7 dakikalık okuma
Ahmet Buğra Kösen
Software Developer

Önceki yazıda Milvaion'ı neden yazdığımı ve mimarisinin nasıl çalıştığını anlatmıştım. Sonunda da şöyle bir söz vermiştim: mevcut scheduler'ını hiç değiştirmeden Milvaion'ın izleme yeteneklerini nasıl ekleyebileceğine bakacağız.

Bu yazı tam olarak o. Ama önce şunu konuşalım: neden böyle bir şey yazma ihtiyacı duydum?


Migration Gerçekten Zor Bir Şey

Yeni bir open-source tool çıkardığında insanlara söylediğin şey aslında şu: "elindeki çalışan sistemi söküp benimkini tak."

Bu, kimsenin sabah kalkıp yapmak isteyeceği bir iş değil. Hele ki söz konusu olan arka plan işleriyse. Hangfire'da beş yıldır sorunsuz çalışan kırk tane job'ın varsa, "daha iyi bir dashboard" uğruna hepsini taşımak mantıklı bir risk/getiri hesabı değil. Ben de olsam yapmazdım.

Ama şunu da biliyorum: o kırk job'ın gece 3'te patladığında ne olduğunu görmek isteyen bir IT Operasyon ekibi var ve şu an ellerinde Seq sorgusundan başka bir şey yok.

Bu ikisi çelişmiyor aslında. İzleme ile çalıştırma ayrı şeyler. Job'ların Hangfire'da çalışmaya devam edebilir, ama o job'lara ne olduğu Milvaion'da toplanabilir.

External Scheduler Integration bunun için var.


Ne Yapıyor, Ne Yapmıyor

Netleştirelim, çünkü bu ayrım önemli:

Yapıyor:

  • Job'larını Milvaion dashboard'ında listeliyor
  • Her çalışmayı (occurrence) statüsü, süresi, exception'ı ve hangi worker'da koştuğuyla birlikte kaydediyor
  • Job'ın içinden yayımladığın logları occurrence'ın altında canlı gösteriyor
  • Metriklere dahil ediyor: EPM, ortalama süre, başarı oranı, statü sayaçları
  • Alarm kurallarını çalıştırıyor — job patladığında Slack/Teams/e-posta bildirimi

Yapmıyor:

  • Job'ı tetiklemiyor. Zamanlamayı hâlâ Hangfire ya da Quartz yapıyor.
  • Cron'unu değiştirmiyor. O senin scheduler'ının kararı.
  • Job'ı silmiyor, iptal etmiyor, duraklatmıyor.

Dashboard'da bu job'lar external rozetiyle görünüyor ve Trigger/Delete butonları kapalı geliyor. Edit ekranında da sadece Milvaion'a ait alanlar açık: display name, description, tags, zombie timeout. Cron, job data, execution timeout, concurrent policy gibi alanlar kilitli — çünkü onları Milvaion yönetmiyor.

Bunu bilinçli olarak böyle yaptım. Dashboard'dan cron'u değiştirebilseydin ama Hangfire'daki gerçek değer değişmeseydi, ekranda yalan söyleyen bir alan olurdu. İki kaynağın çeliştiği bir sistemden daha kötü bir şey yok.


Nasıl Çalışıyor?

Mimari tarafında sürpriz yok, ilk yazıdaki akışın kısaltılmış hali:

Uygulamanın içine bir SDK paketi ekliyorsun. Bu paket Hangfire tarafında bir job filter, Quartz tarafında bir job listener kaydediyor. Job başladığında ve bittiğinde bu filter/listener devreye giriyor, olayı RabbitMQ'ya bir mesaj olarak atıyor.

Milvaion tarafında ExternalJobTrackerService bu mesajları tüketiyor: ilk gördüğünde IsExternal = true işaretli bir ScheduledJob kaydı oluşturuyor, her çalışma için bir JobOccurrence açıyor ve iş bitince statüsünü, süresini, exception'ını güncelliyor.

Uygulaman (Hangfire/Quartz)
└── MilvaionJobFilter / MilvaionJobListener
└── RabbitMQ
└── ExternalJobTrackerService (Milvaion API)
├── ScheduledJob (IsExternal = true)
├── JobOccurrence (status, duration, exception)
└── Dashboard / Alerts / Metrics

Dikkat edilecek nokta şu: bu akış tek yönlü. Milvaion'dan senin uygulamana giden bir komut yok. Bu da entegrasyonun neden bu kadar düşük riskli olduğunun cevabı.

Peki ya entegrasyon patlarsa?

Bunu ayrıca konuşmak istiyorum çünkü ilk sorulan soru bu oluyor: "Milvaion'a bağlanamazsa job'larım durur mu?"

Hayır. Filter'ın içindeki her metot try-catch ile sarılı ve hata durumunda sessizce logluyor:

catch (Exception ex)
{
LogSafeError(ex, "OnPerforming", context?.BackgroundJob?.Job?.Type?.Name);
}

Ayrıca mesaj yayımlama işi fire-and-forget: job'ın thread'i RabbitMQ'yu beklemiyor. RabbitMQ kapalıysa, ağ koptuysa ya da Milvaion API'si down'sa senin job'ın hiçbir şey olmamış gibi çalışmaya devam eder. Sadece o çalışmanın kaydı dashboard'a düşmez.

Mevcut ve çalışan bir sisteme eklenti yaptığın için buradaki tasarım kararı çok net olmalıydı: izleme katmanı hiçbir koşulda çalıştırma katmanını etkilememeli.


Hangfire Entegrasyonu

Pratiğe geçelim. Önce paketi ekle:

dotnet add package Milvasoft.Milvaion.Sdk.Worker.Hangfire

Sonra iki satır:

using Hangfire;
using Milvasoft.Milvaion.Sdk.Worker.Hangfire.Extensions;

var builder = Host.CreateApplicationBuilder(args);

// 1. satır — Milvaion servislerini kaydet
builder.Services.AddMilvaionHangfireIntegration(builder.Configuration);

builder.Services.AddTransient<MyEmailJob>();

builder.Services.AddHangfire((sp, config) =>
{
config.UsePostgreSqlStorage(connectionString);

// 2. satır — filter'ı Hangfire'a tak
config.UseMilvaion(sp);
});

builder.Services.AddHangfireServer(options =>
{
options.WorkerCount = 4;
options.Queues = ["default", "critical"];
});

await builder.Build().RunAsync();

Hepsi bu. AddMilvaionHangfireIntegration core worker servislerini (heartbeat, statü bildirimi, log publisher) kaydediyor ama job consumer'ı kaydetmiyor — çünkü bu worker kuyruktan iş almayacak, sadece Hangfire'ın kendi işlerini raporlayacak. UseMilvaion ise MilvaionJobFilterGlobalJobFilters'a ekliyor.

Job kodunda tek bir satır değişmiyor. MyEmailJob neyse o kalıyor.

Kalan tek şey konfigürasyon:

{
"Worker": {
"WorkerId": "hangfire-worker",
"MaxParallelJobs": 128,
"RabbitMQ": {
"Host": "rabbitmq",
"Port": 5672,
"Username": "guest",
"Password": "guest",
"VirtualHost": "/"
},
"Redis": {
"ConnectionString": "redis:6379"
},
"Heartbeat": {
"Enabled": true,
"IntervalSeconds": 5
},
"ExternalScheduler": {
"Source": "Hangfire"
}
}
}

ExternalScheduler.Source alanı kritik. Milvaion bu job'ların nereden geldiğini buradan biliyor ve dashboard'da bu isimle etiketliyor.

Hangfire tarafında hangi olaylar yakalanıyor?

MilvaionJobFilter üç ayrı Hangfire arayüzünü birden implement ediyor:

ArayüzMetotNe oluyor
IClientFilterOnCreating / OnCreatedJob Milvaion'a kaydediliyor (ExternalJobRegistrationMessage)
IServerFilterOnPerformingOccurrence açılıyor, CorrelationId üretilip job parameter'a yazılıyor
IServerFilterOnPerformedSüre hesaplanıyor, statü ve exception yazılıyor
IElectStateFilterOnStateElectionJob DeletedState'e geçerse occurrence Cancelled işaretleniyor

OnPerforming'de üretilen CorrelationId, Milvaion_CorrelationId adıyla Hangfire'ın job parameter'larına yazılıyor. Birazdan bunu log basmak için kullanacağız.


Quartz.NET Entegrasyonu

Aynı hikâye, farklı bağlantı noktası. Quartz'ta filter yok, listener var:

dotnet add package Milvasoft.Milvaion.Sdk.Worker.Quartz
using Milvasoft.Milvaion.Sdk.Worker.Quartz.Extensions;
using Quartz;

var builder = Host.CreateApplicationBuilder(args);

builder.Services.AddMilvaionQuartzIntegration(builder.Configuration);

builder.Services.AddQuartz(q =>
{
// Milvaion listener'larını devreye al
q.UseMilvaion();

var myJobKey = new JobKey("MyJob", "MyGroup");

q.AddJob<MyJob>(opts => opts.WithIdentity(myJobKey)
.WithDescription("My scheduled job"));

q.AddTrigger(opts => opts.ForJob(myJobKey)
.WithIdentity("MyJob-Trigger")
.WithCronSchedule("0 0 * * * ?"));
});

builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);

await builder.Build().RunAsync();

q.UseMilvaion() iki listener kaydediyor:

  • MilvaionSchedulerListener — scheduler ayağa kalktığında tanımlı job'ları Milvaion'a kaydediyor.
  • MilvaionJobListenerJobToBeExecuted ile occurrence açıyor, JobWasExecuted ile kapatıyor.

appsettings.json tarafında tek fark Source değeri:

"ExternalScheduler": {
"Source": "Quartz"
}

Quartz'ta CorrelationId, job parameter yerine MergedJobDataMap üzerinden geliyor.


Canlı Log Akışı

Buraya kadar anlattığım kısım tamamen pasif: job'ın kodunu hiç açmadan çalışma geçmişi, süreler ve exception'lar dashboard'a düşüyor.

Ama ilk yazıda "en çok değer katan şey canlı log akışı" demiştim. Onu istiyorsan job'ın içine küçük bir dokunuş gerekiyor — ILogPublisher enjekte ediyorsun:

using Hangfire.Server;
using Milvasoft.Milvaion.Sdk.Domain.JsonModels;
using Milvasoft.Milvaion.Sdk.Worker.RabbitMQ;

public class SendEmailJob(ILogger<SendEmailJob> logger, ILogPublisher logPublisher)
{
public async Task ExecuteAsync(PerformContext context, string recipient, CancellationToken ct)
{
// Filter'ın yazdığı tracking bilgisini oku
var correlationIdStr = context.GetJobParameter<string>("Milvaion_CorrelationId");
var workerId = context.GetJobParameter<string>("Milvaion_WorkerId") ?? "hangfire-worker";
var correlationId = Guid.TryParse(correlationIdStr, out var cid) ? cid : Guid.Empty;

await logPublisher.PublishLogAsync(correlationId, workerId, new OccurrenceLog
{
Level = "Information",
Message = $"{recipient} adresine e-posta gönderiliyor",
Timestamp = DateTime.UtcNow,
Category = "UserCode"
});

await SendAsync(recipient, ct);

// Job bitmeden buffer'ı boşalt
await logPublisher.FlushAsync(ct);
}
}

İki nokta:

  • CorrelationId olmadan log gitmez. Boş Guid gelirse publisher sessizce çıkar. Log görünmüyorsa ilk bakılacak yer burası.
  • FlushAsync şart. Loglar performans için buffer'lanıyor; job bitmeden flush etmezsen son satırlar kaybolabilir.

Kabul: bu, Milvaion'ın kendi IAsyncJob'ındaki context.LogInformation(...) kadar zarif değil. Ama pasif izleme için hiçbir şey yapmana gerek yok; log akışı isteğe bağlı bir üst basamak. Kademeli benimseme fikri tam olarak bu.


Denemek İçin Hızlı Yol

Kendi projene dokunmadan görmek istersen hazır image'lar var:

services:
hangfire-worker:
image: milvasoft/milvaion-sample-hangfire-worker:latest
environment:
- Worker__WorkerId=hangfire-worker-1
- Worker__RabbitMQ__Host=rabbitmq
- Worker__Redis__ConnectionString=redis:6379
- Worker__ExternalScheduler__Source=Hangfire
depends_on: [rabbitmq, redis]

quartz-worker:
image: milvasoft/milvaion-sample-quartz-worker:latest
environment:
- Worker__WorkerId=quartz-worker-1
- Worker__RabbitMQ__Host=rabbitmq
- Worker__Redis__ConnectionString=redis:6379
- Worker__ExternalScheduler__Source=Quartz
depends_on: [rabbitmq, redis]

Milvaion'ı docker compose up -d ile ayağa kaldırdıktan sonra bunları ekleyip dashboard'a bakman yeterli. Örnek worker'lar birkaç saniyede bir çalışan demo job'lar içeriyor, ekranda anında hareket görürsün.


Sık Karşılaşılan Üç Sorun

Job'lar dashboard'da hiç görünmüyor. Önce RabbitMQ bağlantısına bak (docker logs <worker> | grep -i rabbitmq), sonra ExternalScheduler.Source değerinin dolu olduğundan emin ol. Boşsa filter erken return ediyor.

Job görünüyor ama occurrence'lar Running'de asılı kalıyor. OnPerformed / JobWasExecuted mesajı ulaşmamış demektir. Uygulama iş ortasında kapandıysa normal — Milvaion'ın zombie detection'ı bir süre sonra bunları Failed'a çeker. Zombie timeout'u external job'larda düzenleyebildiğin birkaç alandan biri, tam da bu yüzden.

Loglar akmıyor. ILogPublisher enjekte edilmiş mi, CorrelationId gerçekten dolu mu, FlushAsync çağrılmış mı. Üçünden biri eksikse log görünmez.


Bu Entegrasyon Neyi Çözüyor?

Dürüst olmak gerekirse burada teknik olarak dâhiyane bir şey yok. Bir filter, bir listener, birkaç RabbitMQ mesajı. Kodun kendisi Milvaion'ın en basit parçalarından biri.

Ama ürün olarak en çok düşündüğüm parça buydu.

Çünkü Milvaion'ın karşısındaki asıl rakip Hangfire ya da Quartz değil — hiçbir şey yapmamak. İnsanların çoğu yeni bir tool'a bakıp "iyiymiş" deyip kapatıyor, çünkü denemenin maliyeti dashboard'ın getirisinden yüksek görünüyor. İki satır kodla mevcut sisteminden hiçbir şeyi riske atmadan sonucu görebiliyorsan, o hesap değişiyor.

Sonrası zaten organik: bir süre external mod'da izlersin, dashboard'ı kullanmaya alışırsın, alarmları kurarsın. Bir gün yeni bir job yazman gerektiğinde belki onu IAsyncJob olarak yazarsın. Belki hiç yazmazsın ve Milvaion senin için sadece bir observability katmanı olarak kalır. İkisi de benim için kabul edilebilir sonuçlar.

Milvaion GitHub'da Apache 2.0 lisansıyla açık kaynak. External scheduler entegrasyonunun tüm detayları için dökümana göz atabilirsin.

Bir sonraki yazıda görüşmek dileğiyle…

Scheduler'ınıza Soru Sormak — Milvaion MCP Server

· 7 dakikalık okuma
Ahmet Buğra Kösen
Software Developer

Serinin ilk yazısında Milvaion'ın çıkış noktasını, ikinci yazıda mevcut Hangfire/Quartz kurulumlarını değiştirmeden nasıl izleyebileceğini anlatmıştım.

Bu yazı, sisteme en son eklediğim ve açıkçası en çok tartıştığım özellik hakkında: MCP server.

Baştan uyarayım — bu bir "yapay zeka ile her şey çözüldü" yazısı değil. Aksine, bu özelliği yazarken en çok uğraştığım kısım ne yapabileceği değil, ne yapamayacağıydı.


Aynı Veriye İkinci Bir Kapı

İlk yazıda şunu söylemiştim: asıl değer job'ları çalıştırmakta değil, çalıştıktan sonra ne olduğunu görebilmekte. Dashboard tam da bunun için var ve üzerine en çok vakit harcadığım kısım.

MCP server onun yerine geçen bir şey değil. Dashboard hâlâ sistemi yönettiğin, workflow'unu kurduğun, job'ını düzenlediğin, canlı log akışını izlediğin yer — ve öyle kalıyor. MCP'nin yaptığı, aynı veriye ikinci bir kapı açmak: bakmak yerine sormak istediğin anlar için.

Aradaki fark şurada beliriyor. Dashboard bir soruyu cevaplamakta çok iyi. Bir teşhis ise genelde tek soru değil, birbirini tetikleyen bir soru zinciri:

Gece daily-invoice-export patlamış. Occurrence ekranında exception'ı okuyorsun — bir timeout. Sonra "bu ne zamandır böyle?" diye geçmişe bakıyorsun, salıdan beri. Sonra "salı günü worker ayakta mıydı?" diye worker ekranına, sonra "salı günü biri bu job'a dokunmuş mu?" diye activity log'a geçiyorsun.

Ekranların her biri sana doğru cevabı veriyor. Zorlanan taraf sen oluyorsun: dört ekrandan topladığın parçaları kafanda birleştirmek. Dördüncüde, birincide ne gördüğünü hatırlamaya çalışıyorsun.

Oysa aklındaki soru en baştan beri tek cümleydi: "daily-invoice-export salıdan beri patlıyor, logları oku ve ne değiştiğini söyle."

MCP server bu cümleyi doğrudan çalıştırılabilir hale getiriyor. Gezinmenin kendisini değil, gezinirken bağlamı taşıma yükünü ortadan kaldırıyor.

Kimin ne zaman hangi kapıyı kullanacağı da net: bir şeyi değiştirecekseniz dashboard, bir şeyi anlayacaksanız ikisi de iş görür — hangisi o an daha hızlıysa.


Kurulum

1. Read-only bir API key üret

Dashboard'dan bir API key oluştur ve sadece şunları ver:

  • ScheduledJobManagement.List, ScheduledJobManagement.Detail
  • FailedOccurrenceManagement.List
  • WorkerManagement.List
  • WorkflowManagement.List

Bu setle asistan her şeyi inceleyebilir, hiçbir şeyi değiştiremez. Çoğu insan için doğru varsayılan bu.

2. Sunucuyu editörüne tanıt

Milvaion remote HTTP bir MCP server, yani streamable HTTP transport destekleyen her client bağlanabilir. Claude Code'da dosya düzenlemeye bile gerek yok:

claude mcp add --transport http milvaion \
https://milvaion.sirketiniz.com/mcp \
--header "X-ApiKey: $MILVAION_API_KEY"

Cursor için .cursor/mcp.json:

{
"mcpServers": {
"milvaion": {
"type": "http",
"url": "https://milvaion.sirketiniz.com/mcp",
"headers": { "X-ApiKey": "your-api-key" }
}
}
}

Burada can sıkıcı bir gerçek var: her client aynı şeyi farklı anahtar isimleriyle istiyor. VS Code/Copilot üst seviye anahtar olarak mcpServers değil servers bekliyor. Windsurf url değil serverUrl diyor. Gemini CLI ise httpUrl.

"Bağlandı ama hiç tool görünmüyor" şikayetlerinin bir numaralı sebebi bu. İkinci sebebi ise Copilot'ta chat'in Agent moduna alınmamış olması — Ask modunda MCP tool'ları hiç görünmüyor.

Detaylı client matrisi ve Claude Desktop/ChatGPT'nin kendine has kısıtları için dökümanda ayrı bir bölüm var.

Anahtarı repoya koyma. Yukarıdaki client'ların hepsi ya ortam değişkeni genişletmeyi ya da çalışma anında sorma özelliğini destekliyor. İçinde canlı key olan bir config dosyası er ya da geç commit'lenir ve git geçmişine giren bir key'in tek çaresi yenisini üretip eskisini iptal etmektir.

3. Bir şey sor

Dün gece hangi job'lar patladı?

daily-invoice-export salıdan beri patlıyor. Logları oku ve ne değiştiğini söyle.

SendReportJob'ı çalıştırabilecek ayakta bir worker var mı?


Pratikte Neye Benziyor?

İlk soruyu sorduğunda asistan tek bir tool çağırmıyor. Tipik akış şöyle:

list_failures ile dead letter kayıtlarını çekiyor → aynı job'ın birden fazla kaydını görüp list_occurrences ile geçmişe bakıyor → patlamanın ne zaman başladığını buluyor → get_occurrence ile o çalışmanın loglarını ve exception detayını alıyor → list_workers ile worker'ın o sırada ayakta olup olmadığını kontrol ediyor → list_activity_logs ile o tarihte birinin job'a dokunup dokunmadığına bakıyor.

Yani dashboard'da ekran ekran gezerek yaptığın zinciri, sırayla kendisi kuruyor. Farkı, bağlamı kafanda taşımak zorunda olmaman.

Şunun altını çizeyim: bu, sihirli bir teşhis değil. Asistan logda ne yazıyorsa onu okuyor — dashboard'da senin gördüğün veriyle birebir aynı veri. Ama "bu exception'ı ilk gördüğüm tarih" ile "o tarihte activity log'da ne var" arasındaki bağlantıyı kurmak, sabah 9'da kahvesini içmemiş bir insandan daha hızlı yaptığı bir iş.

Cevabı aldıktan sonra genelde yine dashboard'a geçiyorsun zaten — çünkü job'ı düzeltmek, cron'u değiştirmek ya da tekrar tetiklemek orada yapılıyor.


Tool'lar ve İzin Modeli

40'tan fazla tool var ve hepsi bir izne bağlı. Kabaca gruplar:

GrupÖrnekler
Okumaget_overview, list_jobs, get_job, list_occurrences, get_occurrence, list_failures, list_workers, search_logs, summarize_logs, get_latest_report, list_activity_logs
Sistemget_system_health, get_queue_stats, get_database_statistics, get_configuration
Çalıştırmatrigger_job, cancel_occurrence, set_job_active, trigger_workflow
Düzenlemecreate_job, update_job, resolve_failures
Silmedelete_job, delete_occurrences, delete_failures, delete_worker

Buradaki tasarım kararı şu: hangi tool'ların görüneceğini sen seçmiyorsun, key'in ne yapabileceğini seçiyorsun.

Sadece List ve Detail izni verdiğin bir key'le asistanın önünde on beş okuma tool'u kalıyor, gerisi yok. İzni olmayan bir tool'u çağırdığında dönen hata da eksik iznin adını söylüyor — böylece model körlemesine tekrar denemek yerine sana "şu izni vermen lazım" diyebiliyor.

Bir de fark edilmesi zor ama önemli bir detay: get_occurrence logun tamamını değil, varsayılan olarak son 100 satırını döndürüyor (logLines ile artırabiliyorsun). Döngü içinde log basan bir job, aksi halde asistanın tüm context'ini tek çağrıda doldurabilir.


Prompt'lar — Beklediğimden Daha Önemli Çıktı

Sunucuyla birlikte üç tane prompt template geliyor:

PromptNe yapıyor
diagnose_jobPatlayan bir job'ı sırayla geziyor: failure'lar, örüntü, loglar, worker sağlığı, sonra son konfigürasyon değişiklikleri
overnight_reviewBelirli bir zaman aralığını inceleyip hataları tek tek listelemek yerine sebebe göre grupluyor
explain_workflowBir workflow'un adımlarını, dallanmasını ve veri akışını düz cümleyle anlatıyor

Bunlar ilk bakışta "hazır soru şablonu" gibi duruyor ama işlevleri farklı. Kendi haline bırakıldığında model genelde ilk okuduğu tool'dan başlıyor — list_jobs çağırıp job listesini geziyor, oysa sorunun cevabı list_failures'ta.

Prompt'lar modelin önüne bir çalışma sırası koyuyor. Aradaki fark, doğru cevap ile üç fazladan tool çağrısı sonrası doğru cevap arasındaki fark.


Bilinçli Olarak Dışarıda Bıraktıklarım

Kullanıcı yönetimi, roller, izinler, API key'ler, dispatcher kontrolü ve konfigürasyon yönetimi için hiç tool yok. Key'e ne yetki verirsen ver, bunlara MCP üzerinden erişilemiyor. Sebep basit: bir asistanın yeni bir API key üretebilmesi ya da kendi izinlerini genişletebilmesi, düşünmek bile istemediğim bir senaryo.

Workflow oluşturma ve düzenleme de yok. Condition'ları ve data mapping'leri olan yönlü bir grafiği tool çağrılarıyla kurmak teknik olarak mümkün ama doğru yer görsel builder. Bir asistanın "sanırım bu iki node'u bağlamak istedin" tahmini üzerine kurulmuş bir workflow, kimsenin production'da istemeyeceği bir şey.

Birkaç güvenlik detayı daha:

  • Yan etkiler kime ait olduğu yazılarak kaydediliyor. MCP üzerinden tetiklenen, iptal edilen ya da çözüldü işaretlenen her şey key'in adıyla loglanıyor. Geçmişte "bunu insan mı yaptı asistan mı" sorusunun cevabı duruyor.
  • Concurrency politikaları asla atlanmıyor. trigger_job her zaman force kapalı çalışıyor. Bir job'ın eşzamanlılık politikasını ezmek bilinçli bir insan eylemi olarak dashboard'da kalıyor.
  • Alanlar kazara temizlenemiyor. update_job sadece kendisine verilen argümanları değiştiriyor; verilmeyen alan boşaltılmıyor, olduğu gibi bırakılıyor.

Ve dürüst olmam gereken kısım:

Tool açıklamaları modele "bunu yapmadan önce onay al", "delete_job yerine set_job_active tercih et" diyor. Ama bunlar prompt, garanti değil. Modelin bunlara uyacağına dair bir taahhüt veremem.

Bu yüzden tavsiyem net: Create, Update ve Delete izinlerini, aynı yetkiyi bir insana dashboard üzerinden vermekten rahatsız olmayacağın yerlerde ver. Trigger gerçek işin çalışmasına sebep olur. Delete geri alınamaz. Geri kalan her durumda doğru cevap read-only bir key.


Çalıştığını Doğrulamak

/mcp HTTP üzerinden JSON-RPC konuşuyor, yani bir client'a hiç bulaşmadan curl ile test edebilirsin:

curl -X POST http://localhost:5000/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-H "X-ApiKey: $MILVAION_API_KEY" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'

Header'ı çıkardığında aynı isteğin 401 dönmesi, endpoint'in korunduğunun en hızlı doğrulaması.

Sunucu stateless modda çalışıyor; herhangi bir API replikası herhangi bir isteği karşılayabiliyor, load balancer arkasında sticky session gerekmiyor. Bunun bedeli, kalıcı session gerektiren sampling ve elicitation gibi özelliklerin desteklenmemesi.


Sonuç

MCP server'ı yazarken kendime sorduğum soru "yapay zekayı nasıl eklerim" değildi. Soru şuydu: kafamda tek cümle olan bir soruyu, gerçekten tek cümlede sorulabilir hale nasıl getiririm?

Cevabın MCP olması biraz da zamanlama meselesi — protokol ortada, ekiplerin çoğunda zaten Claude Code ya da Cursor açık. Milvaion'ın yapması gereken tek şey, verisini bu ekosisteme düzgün bir şekilde açmaktı. Kendi LLM entegrasyonunu yazmak yerine veri kaynağı olmak, hem daha az kod hem daha az sorumluluk.

İşin ilginç yanı şu: bu özelliği yazarken harcadığım zamanın çoğu tool'ları eklemekle değil, hangilerini eklememem gerektiğine karar vermekle geçti. Bir scheduler'a doğal dil arayüzü açmak kolay; onu production'da güvenli tutmak asıl iş.

Milvaion GitHub'da Apache 2.0 lisansıyla açık kaynak. MCP server'ın tüm tool listesi, client konfigürasyonları ve güvenlik modeli için dökümana göz atabilirsin.

Bir sonraki yazıda görüşmek dileğiyle…

Neden Bir Distributed Job Scheduler Yazdım?

· 11 dakikalık okuma
Ahmet Buğra Kösen
Software Developer

.NET tarafında arka plan işleri denince akla gelen ilk iki isim belli: Hangfire ve Quartz.NET. İkisi de olgun, ikisi de yıllardır sahada. Peki ortada bunlar varken neden oturup bir job scheduler yazdım?

Kısa cevap: ikisiyle de bir sorunum yoktu. Sorun, işlerin nerede çalıştığıyla başladı. Ama orada bitmedi — asıl uzun hikâye, o ilk sorunu çözdükten sonra ortaya çıkan eksiklerdi.

Bu yazıda önce o ilk sorunu anlatacağım, sonra Milvaion'ın buna nasıl bir cevap verdiğine bakacağız.


Problem: Job'lar Uygulamanın İçinde Çalışıyor

Hangfire'ı ya da Quartz'ı bir projeye eklediğinde, scheduler senin uygulamanın process'inin içinde yaşar. Zamanı gelen job, aynı process'te, aynı thread pool'da çalışır.

Bu, işlerin çoğu için gayet iyidir. Ta ki şunlardan biri olana kadar:

  • Uzun süren bir job diğerlerini bekletir. Gece çalışan rapor job'ın iki saat sürüyorsa, o iki saat boyunca thread pool'dan pay alır.
  • Patlayan bir job scheduler'ı da götürür. Job'ın içinde yakalanmamış bir exception ya da bir memory leak, sadece o job'ı değil uygulamanın tamamını etkiler.
  • Farklı işler farklı donanım ister. ML modeli çalıştıran bir job GPU isterken, e-posta gönderen bir job'ın 128 MB RAM'i vardır. İkisi aynı process'te yaşıyorsa, en pahalı olanın ihtiyacına göre ölçeklenirsin.
  • Job'lar API ile birlikte ölçeklenir. API'ye gelen trafik arttı diye pod sayısını üçe çıkardığında, job'ların da üçe çıkar. Genelde istediğin bu değildir.

Benim durumumda dört maddenin dördü de vardı. Bir noktada şunu fark ettim: aslında istediğim şey job'ların nerede çalışacağına ayrı karar verebilmekti.


Milvaion Nedir?

Milvaion, .NET 10 üzerine yazılmış, açık kaynak (Apache 2.0) bir distributed job scheduling sistemi. Temel fikri tek cümleyle şu:

Job'ın ne zaman çalışacağına karar veren yer ile onu çalıştıran yer aynı process olmak zorunda değil.

Bunu ikiye ayırıyor:

  • Scheduler (API): Cron ifadelerini okur, zamanı gelen job'ı tespit eder, kuyruğa atar. Dashboard'u da o barındırır.
  • Worker: Kuyruktan mesajı alır, senin IAsyncJob kodunu çalıştırır, sonucu geri bildirir.

Aralarında RabbitMQ, Redis ve PostgreSQL var.

Milvaion Dashboard

Bu ayrımın pratikte karşılığı şu: worker'ı ayrı deploy edersin, ayrı ölçeklersin, ayrı donanıma koyarsın. GPU isteyen job'lar için GPU'lu worker, e-posta için 128 MB'lık worker. Bir worker çökerse scheduler etkilenmez, kuyruktaki mesaj bekler.


Temel Kavramlar

Milvaion'da dolaşırken karşına çıkacak dört kelime var, baştan netleştirelim:

KavramNe Demek
JobTekrarlayan ya da tek seferlik bir çalışma tanımı. "Her sabah 9'da rapor gönder" bir job'dır.
Worker JobIAsyncJob implement eden C# sınıfın. Asıl işi yapan kod.
OccurrenceBir job'ın tek bir çalışması. Statüsü, süresi ve logları vardır.
WorkerJob'ları çalıştıran process.

Job bir tanım, occurrence ise o tanımın bir kez koşması. Dashboard'da "bu job 340 kere çalışmış, 3'ü patlamış" derken bahsedilen şey occurrence'lar.


Nasıl Çalışıyor?

Akış şöyle işliyor:

  1. Worker Auto Discovery senin worker'ını ve içindeki job sınıflarını otomatik keşfeder.
  2. Dashboard'dan ya da REST API'den bir job oluşturursun.
  3. Scheduler bunu PostgreSQL'e yazar ve bir sonraki çalışma zamanıyla Redis ZSET'e ekler.
  4. Dispatcher Redis'i kontrol eder, zamanı gelenleri bulur.
  5. Zamanı gelen job'lar routing key ile RabbitMQ'ya publish edilir.
  6. Worker mesajı alır, senin IAsyncJob kodunu çalıştırır.
  7. Worker durum ve logları RabbitMQ üzerinden geri bildirir.
  8. Scheduler sonucu kaydeder ve SignalR ile dashboard'a anlık olarak yansıtır.

Zamanlama için Redis ZSET kullanmamın sebebi, "şu ana kadar zamanı gelmiş job'ları getir" sorgusunun ZSET'te son derece ucuz olması. Veritabanını her saniye yoklamak yerine Redis'e soruyorum.


Bir Worker Yazmak

Teoriyi bir kenara bırakıp koda bakalım. Önce template'i kuralım:

dotnet new install Milvasoft.Templates.Milvaion
dotnet new milvaion-console-worker -n MyCompany.MyWorker

Sonra bir job yazalım:

using Milvasoft.Milvaion.Sdk.Worker.Abstractions;

public class SendReportJob(IReportService reportService) : IAsyncJob
{
public async Task ExecuteAsync(IJobContext context)
{
context.LogInformation("Rapor hazırlanıyor...");

// Job'a dashboard'dan girilen JSON payload'ı tipli olarak alıyoruz
var data = context.GetData<ReportRequest>();

await reportService.GenerateAsync(data, context.CancellationToken);

context.LogInformation("Rapor gönderildi.");
}
}

Dikkat edilecek üç nokta var:

  • Constructor injection çalışıyor. Worker normal bir .NET host olduğu için, DI container'ına ne koyduysan job'ın içinde kullanabilirsin.
  • context.LogInformation ile yazdığın loglar dashboard'da o occurrence'ın altında, canlı olarak görünür. Teknik loglar ayrıca Seq'e gider.
  • context.CancellationToken önemli. Job'ı dashboard'dan iptal ettiğinde ya da timeout'a düştüğünde tetiklenen token bu. Uzun süren işlerde bunu aşağıya geçirmezsen job iptal edilemez.

Worker'ı çalıştırdığında auto discovery devreye girer ve SendReportJob dashboard'da seçilebilir hale gelir. Ayrıca bir kayıt işlemi yapmana gerek yok.


Asıl Mesele Distributed Olması Değildi

Buraya kadar anlattığım şey Milvaion'ın çıkış noktasıydı. Ama dürüst olayım: eğer tek yaptığı job'ları ayrı process'te çalıştırmak olsaydı, muhtemelen bu yazıyı yazmaya değmezdi.

Scheduler'ı ayırdıktan sonra fark ettiğim şey şu oldu — asıl zamanı yiyen kısım job'ın nerede çalıştığı değil, çalıştıktan sonra ne olduğuydu. Job patladı, e-posta geldi, sonra ne? Logu nerede? Hangi worker'daydı? Dün de patlamış mıydı? Bunu kim değiştirmiş?

Hangfire'da ve Quartz'ta bu soruların cevabı genelde "Seq'e bak" ya da "veritabanına gir" gibi daha teknik bilgi sahibi insanlara yönelik oluyor. Fakat örneğin mevcut şirketimde IT Operasyon ekibimizin Hangfire altyapısında çalışan mevcut scheduler'ımızı anlamakta ve kullanmakta zorlandığını gördüm. Zamanla ortaya çıkan özelliklerin çoğu bu boşluğu kapatmak için yazıldı. Bu tarz tam kapsamlı piyasada gördüğüm sadece temporal.io oldu. Bende o devasa ekosisteme girmek ve öğrenmek yerine sfırdan, daha lightweight, developer friendly ve kontrolün tamamen bende olduğu bir tool geliştireyim dedim :)

Dashboard

Bu, üzerinde en çok vakit harcadığım kısımlardan biri. Quartz.NET'in hazır bir arayüzü yok — üçüncü parti bir şey kurarsın ya da kendin yazarsın. Hangfire'ın var ve gayet iş görüyor, ama kapsamı belli: job listesi, retry, recurring job'lar.

Milvaion'ın dashboard'ı şunları yapıyor:

  • Canlı log akışı. Job çalışırken context.LogInformation satırları SignalR üzerinden ekrana düşüyor. Bitmesini bekleyip Seq'e gitmiyorsun.
  • Occurrence geçmişi. Her çalışmanın süresi, statüsü, exception'ı, hangi worker'da koştuğu. Filtrelenebilir, cursor pagination'lı — 30 bin kayıtlı bir job'da da açılıyor.
  • Worker sağlığı. Hangi worker ayakta, kaç job çalıştırıyor, kapasitesi ne, heartbeat'i ne zaman geldi. Mcp server ile bu soruların cevabını yapay zeka ile alabilir ve tüm sistemi yönetebilirsin ;)
  • Job'ı ekrandan düzenleme. Cron'u değiştir, duraklat, tetikle, job data'sını güncelle. Deploy gerekmiyor.
  • Enterprise yönetim. RBAC based role yönetimi, kullanıcı yönetimi, auditing, api key yönetimi, mcp server, failure takibi gibi tüm sistemi arayüz üzerinden no-code yönetmenizi sağlayan bir çok ekran mevcut.

Bunlar tek tek "vay be" dedirtecek şeyler değil. Ama bir job gece 3'te patladığında hepsinin aynı ekranda olmasıyla olmaması arasında ciddi fark var.

Workflow Engine (DAG)

Hangfire'da ContinueJobWith var, yani "bu bitince şunu çalıştır" zinciri kurabiliyorsun. Quartz'ta bunun karşılığı yok, kendin kurgularsın.

Aslında workflow engine geliştirmeyi hiç istemiyordum çünkü n8n gibi robust toollar zaten piyasada mevcut fakat sisteme job chaning eklenmeliydi ve job chaning'in ucu eninde sonunda buraya çıkıyor. E madem job chaning yapacağız düzgün yapalım dedim :)

Milvaion'da görsel bir DAG builder var. Adımları sürükleyip bağlıyorsun; condition node ile koşula göre dallanıyor, merge node ile dallar birleşiyor, adımlar arasında data mapping ile bir adımın çıktısını diğerine bağlıyorsun. Bir adım patladığında hangi adımların çalışmadığı ekranda duruyor.

"Önce veriyi çek, başarılıysa işle, hata varsa alarm at, ikisinden sonra raporu gönder" gibi bir akış, üç ayrı job ve aralarında elle yazılmış kontrol koduyla uğraşmadan kuruluyor.

Alerting

Job patladığında birinin haberi olması lazım. Milvaion'da alarm kanalları built-in: Google Chat, Microsoft Teams, Slack, e-posta ve uygulama içi bildirim.

Hangfire ve Quartz tarafında bunu genelde kendin yazarsın — bir IJobFilter ya da bir listener, sonra HTTP client, sonra rate limiting derken küçük bir proje olur.

Auto Disable

Bu, en sevdiğim küçük özellik. Sürekli patlayan bir job'ı, belirlediğin eşikten sonra otomatik olarak devre dışı bırakıyor.

Neden önemli: connection string'i bozulmuş bir job her 5 dakikada bir çalışıp gece boyunca 96 kere patlar ve 96 alarm üretir. Sabah geldiğinde inbox'ında 96 mail olur ve içlerinden gerçekten önemli olanı bulamazsın. Auto disable "3 kere üst üste patladıysa dur, birine haber ver" diyor.

Eşiği ve failure window'u job bazında ayarlıyorsun — yani "son 30 dakika içinde 3 kere" diyebiliyorsun ki geçen haftadan kalma hatalar sayıya dahil olmasın.

Zombie Detection ve Timeout'lar

Worker çöktüğünde Running statüsünde asılı kalan occurrence'lar oluşur. Milvaion bunları tespit edip Failed'a çekiyor. İki ayrı timeout var:

  • Execution timeout: Job şu kadar saniyeden uzun sürerse worker cancellation token'ı tetikler.
  • Zombie timeout: Job şu kadar dakikadır kuyrukta ya da çalışıyor görünüyorsa, artık ölmüş kabul edilir.

Concurrent Execution Policy

Bir job'ın önceki koşusu hâlâ devam ederken zamanı tekrar geldiğinde ne olacak? Milvaion'da bunu job bazında seçiyorsun: yenisini atla, sıraya al ya da paralel çalıştır.

Hangfire'da bunu DisableConcurrentExecution attribute'u ile kod tarafında yaparsın, Quartz'ta [DisallowConcurrentExecution] ile. İkisi de derleme zamanı kararı; Milvaion'da ekrandan değiştiriyorsun.

RBAC, Kullanıcılar ve API Key'ler

Dashboard'ı ekibe açtığın anda "kim neyi silebilir" sorusu geliyor. Milvaion'da rol tabanlı yetkilendirme var ve granularity job/worker/dashboard seviyesinde. Birine sadece "görüntüleme" yetkisi verip production'a dokunamamasını sağlayabiliyorsun.

Bunun yanında API key desteği var: CI pipeline'ları, script'ler ve otomasyon için, kullanıcı hesabı olmadan. Key'in ne yapabileceğini yine permission bazında kısıtlıyorsun, iptal edilebiliyor, son kullanma tarihi verilebiliyor.

Hangfire'ın dashboard'ı varsayılan olarak yetkilendirmesiz gelir; IDashboardAuthorizationFilter yazarsın. Quartz'ta ortada bir dashboard olmadığı için soru da yok.

Metrik Raporları

Arka planda çalışan bir reporter worker düzenli olarak rapor üretiyor: hata oranı trendi, süre yüzdelikleri (p50/p95/p99), en yavaş job'lar, worker throughput'u ve doluluk oranı.

Bunlar dashboard'da grafik olarak duruyor. "Bu job son bir haftada yavaşladı mı" sorusuna bakarak cevap verebiliyorsun, sorgu yazmadan.

MCP Server — Scheduler'a Soru Sormak

Bu, en yeni eklenen şey. Milvaion bir MCP (Model Context Protocol) server olarak da çalışıyor. Claude Code, Cursor veya GitHub Copilot'ı Milvaion'a bağlayıp scheduler'ına düz cümleyle soru sorabiliyorsun.

40'tan fazla tool var: job listeleme, execution geçmişi, log okuma, dead letter kayıtları, worker sağlığı, tetikleme, duraklatma, düzenleme. Her biri kendi yetkisinin arkasında — sadece okuma yetkisi verdiğin bir API key'le asistan her şeyi inceleyebilir ama hiçbir şeyi değiştiremez.

Önemli bir not: Milvaion burada veri kaynağı. Sunucu tarafında hiçbir model sağlayıcı anahtarı tutulmuyor, dışarıya hiçbir çağrı yapılmıyor. Model senin editöründe, senin aboneliğinle çalışıyor.

Pratikte şuna benziyor: "dün gece hangi job'lar patladı ve neden" diye soruyorsun, asistan list_failures ile başlayıp get_occurrence ile logları çekiyor ve sana özetliyor.

Hangfire ve Quartz'ı İzlemek

Sonuncusu biraz ters köşe: Milvaion, Hangfire ve Quartz.NET kurulumlarını izleyebiliyor.

İki satır kodla mevcut scheduler'ını read-only şekilde Milvaion'a bağlıyorsun. Job'ların yine Hangfire'da çalışmaya devam ediyor, ama execution geçmişi, loglar, metrikler ve alarmlar Milvaion dashboard'ında toplanıyor. Migration yok, job kodun değişmiyor.

Bunu şunun için yazdım: Milvaion'un amacı hiçbir zaman sektör standardı olmuş Quartz ve Hangfire'a rakip olmak değildi. Bu rekabete girmekte yeni çıkmış bir open-source tool açısından anlamsız zaten. Hali hazırda sistemlerinde bu kütüphaneleri kullanan kişilerin Milvaion migration'ı zor olacak, bu yüzden bu geçişi organik bir şekilde yapabilmek ve Milvaion'un yeteneklerini kullanıcılara gösterecek bir onboarding amaçlı yazıldı bu entegrasyon.


Peki Ya Güvenilirlik?

Dağıtık bir sisteme geçtiğinde "mesaj kayboldu mu?" sorusu kaçınılmaz olarak gelir. Milvaion tarafında bunun cevapları şunlar:

  • At-least-once delivery: RabbitMQ manuel ACK kullanılıyor. Worker işi bitirmeden ACK göndermiyor, dolayısıyla worker ortasında çökerse mesaj kuyruğa geri düşer.
  • Otomatik retry: Exponential backoff ile. Kaç kere deneneceğini job bazında ayarlayabilirsin.
  • Dead Letter Queue: Retry hakkı biten job'lar DLQ'ya düşer, kaybolmaz. Dashboard'da ayrı bir ekranda listelenir.
  • Offline resilience: Worker RabbitMQ'ya ulaşamazsa sonuçları lokal SQLite'a yazar, bağlantı gelince gönderir.

Dikkat Edilmesi Gerekenler

Dürüst olmak gerekirse Milvaion her senaryo için doğru araç değil. Şu durumlarda başka bir şey kullanmalısın:

  1. Tek uygulama, tek sunucu. PostgreSQL + Redis + RabbitMQ üçlüsünü ayağa kaldırmanın maliyeti, kazandığından fazlaysa Hangfire çok daha doğru tercih.
  2. Sub-second zamanlama. Dispatcher en az saniyede bir kontrol ediyor. Milisaniye hassasiyeti gerekiyorsa buraya bakma.
  3. Event işleme. "Şu event gelince şunu çalıştır" ihtiyacın varsa bu bir scheduler işi değil; Kafka ya da benzeri bir şey daha uygun.
  4. .NET Framework. Milvaion .NET 10 hedefliyor. Legacy bir uygulaman varsa Hangfire ve Quartz çok daha geniş TFM desteği sunuyor.

Buna karşılık şu durumlarda gerçekten işe yarıyor: job'ların birden fazla servise dağılmışsa, uzun süren işlerin varsa, farklı job'lar farklı donanım istiyorsa, modern bir uygulama ve mimari istiyorsan, ya da denetim için tam bir çalışma geçmişi tutman gerekiyorsa.


Sonuç

Milvaion'ı yazma sebebim "daha iyi bir Hangfire" yapmak değildi. Zamanlama ile çalıştırmayı ayırmak istedim, çünkü elimdeki problem tam olarak oydu.

Ama işin bana öğrettiği şey şu oldu: o ayrımı yapmak işin sadece başlangıcıydı. Asıl değer, job'ları çalıştırdıktan sonra onlara ne olduğunu görebilmekte — dashboard'da, alarmlarda, raporlarda, auto disable gibi küçük ama gece uykunu kurtaran detaylarda.

Ortaya çıkan şey şu an .NET 10 üzerinde çalışıyor, Apache 2.0 lisanslı ve GitHub'da açık kaynak. Docker Compose ile beş dakikada ayağa kalkıyor:

git clone https://github.com/Milvasoft/milvaion.git
cd milvaion
docker compose up -d

Dashboard http://localhost:5000 adresinde seni bekliyor olacak.

Tüm özellikler için hemencecik ayaklandırıp inceleyebilirsin ya da dökümana bir göz at derim.

Bu yazıda temel mimariden ve özelliklerden bahsettim. Serinin bir sonraki yazısında, mevcut Hangfire ya da Quartz.NET kurulumunu hiç değiştirmeden Milvaion'ın izleme yeteneklerini nasıl ekleyebileceğine bakacağız — çünkü Milvaion'ı benimsemek için migration yapmak zorunda değilsin.

Bir sonraki yazıda görüşmek dileğiyle…

UnitsNet

· 3 dakikalık okuma
Ahmet Buğra Kösen
Software Developer

Fiziksel büyüklüklerle çalışan uygulamalarda birim dönüşümleri zorlayıcı olabilir. .NET platformu için geliştirilen UnitsNet kütüphanesi, farklı ölçüm sistemleri ve fiziksel büyüklükler arasında dönüşümü kolay ve güvenilir hale getirir. Bu makalede, UnitsNet kütüphanesinin özelliklerini ve kullanım alanlarını inceleyeceğiz.

UnitsNet Nedir?

UnitsNet, .NET uygulamalarında fiziksel büyüklüklerin birim dönüşümlerini basit ve güvenilir bir şekilde gerçekleştirmek için geliştirilmiş bir open-source bir kütüphanedir. Birçok ölçüm türünü destekler ve farklı birimler arasında dönüşüm yapmayı sağlar. Örneğin, metre cinsinden bir uzunluğu km olarak almak veya kilogram cinsinden bir ağırlığı pound’a çevirmek gibi işlemler UnitsNet ile son derece kolaydır.

Desteklenen Birim Türleri

UnitsNet kütüphanesi, geniş bir yelpazede fiziksel büyüklükleri ve birimleri destekler. Bu büyüklükler arasında aşağıdakiler yer alır:

  • Uzunluk (Length): metre, kilometre, mil, inç, fit vs.
  • Ağırlık/Kütle (Mass): kilogram, gram, ton, pound vs.
  • Sıcaklık (Temperature): Celsius, Fahrenheit, Kelvin vs.
  • Hacim (Volume): litre, mililitre, galon, kübik metre vs.
  • Alan (Area): metrekare, hektar, akre vs.
  • Basınç (Pressure): Pascal, bar, atm, psi vs.
  • Hız (Speed): metre/saniye, kilometre/saat, mil/saat vs.
  • Enerji (Energy): joule, kalori, kilowatt-saat vs.
  • Güç (Power): watt, kilowatt, beygir gücü vs.
  • Veri Boyutu (Information): byte, kilobyte, megabyte, gigabyte, terabyte vs.

UnitsNet, yukarıdaki büyüklükler ve daha fazlası için destek sağlar ve böylece çok çeşitli mühendislik ve bilimsel hesaplamalar için uygundur.

UnitsNet Implementasyon

Öncelikle, projeye UnitsNet NuGet paketini ekleyelim;

dotnet add package UnitsNet

UnitsNet ile bir büyüklüğü tanımlayıp, farklı birimlere dönüştürmek oldukça basit;

using UnitsNet;

class Program
{
static void Main()
{
// Define 10km
var distance = Length.FromKilometers(10);

Console.WriteLine($"Meters: {distance.Meters}");
// Meters: 10000

Console.WriteLine($"Miles: {distance.Miles}");
// Miles: 6.2137119223733395

Console.WriteLine($"Yard: {distance.Yards}");
// Yard: 10936.132983377078


// Define 100 MB
var fileSize = Information.FromMegabytes(100);

Console.WriteLine($"Byte: {fileSize.Bytes}");
// Byte: 100000000

Console.WriteLine($"Gigabyte: {fileSize.Gigabytes}");
// Gigabyte: 0.1
}
}

Bu kodda, Length.FromKilometers(10) ile 10 kilometre cinsinden bir uzunluk tanımlıyoruz ve distance.Meters, distance.Miles gibi özelliklerle farklı birimlerde değerlerini görüntülüyoruz. Ayrıca, 100 MB’lik bir dosya boyutunu byte ve gigabyte olarak çevrimlerini yapıyoruz.

Daha fazlası için lütfen projenin github sayfasına göz atın…

UnitsNet Kullanırken Dikkat Edilmesi Gerekenler

Kullanım sırasında aşağıdaki konulara dikkat etmek faydalı olacaktır:

  1. Doğru Büyüklüğü Seçmek: Her büyüklük için ayrı bir sınıf vardır (Length, Mass, Temperature gibi). Hangi büyüklüğü kullanacağınızı doğru seçtiğinizden emin olun.
  2. Birim Hassasiyeti: UnitsNet, bazı birim dönüşümlerinde yuvarlama yapabilir. Eğer çok yüksek hassasiyet gerektiren işlemler yapıyorsanız, sonuçları kontrol etmenizde fayda var.
  3. Performans: UnitsNet, büyük veri setleriyle çalışırken dönüşüm işlemlerinin optimize edilmesi gerekebilir. Özellikle büyük döngüler içinde birim dönüşümü yapılacaksa performans testleri yapmakta fayda var.

Sonuç

UnitsNet, .NET platformunda birim dönüşümlerini güvenilir ve kolay bir şekilde yapmak isteyen geliştiriciler için harika bir çözümdür. Geniş bir birim desteği, basit kullanım ve güçlü dönüşüm özellikleri sayesinde, bilimsel, mühendislik ve günlük uygulamalarda kolaylık sağlar. Projelerinizde birim dönüşümü ihtiyacı varsa, UnitsNet'i denemenizi öneririm.

Bir sonraki yazıda görüşmek dileğiyle…

Expo ile Bildirimlere Özel Ses Ekleme

· 2 dakikalık okuma
Ali Burhan Keskin
Software Developer

captionless image

Expo kullanarak mobil uygulamanıza bildirim eklemek oldukça kolay Bildirimlerle çalışırken, özel ses eklemek, kullanıcı deneyimini kişiselleştirmek için harika bir yöntem olabilir. Ancak Expo’da, özellikle Android cihazlarda bazı özel ayarlar yapmanız gerekebilir.

Bu yazıda, Expo projenize bildirimlerde özel ses eklemeyi adım adım anlatacağız.

Not: Bu makale, expo-notifications paketini ve gerekli izinleri ayarladığınızı varsaymaktadır. Özel bildirim seslerinin sadece EAS Build ile desteklendiğini unutmayın. (Bkz. "Custom notification sounds are only supported when using EAS Build.")

1. Ses Dosyalarını Ekleme ve app.config Ayarları

Öncelikle, ses dosyanızı proje dosya yapısına göre ekleyin. Örneğin, src/assets/sounds/bip.mp3 yolunu kullanabilirsiniz. Daha sonra, app.config dosyanızda bu dosya yolunu belirtmeniz gerekecek:

export default ({ config }: ConfigContext): ExpoConfig => ({
...config,
assetBundlePatterns: ["./src/assets/sounds/*"],
plugins: [ [ "expo-notifications",
{
sounds: ["./src/assets/sounds/bip.mp3"],
},
],
],
});

2. Bildirim Konfigürasyonları

En kritik bölüm bildirim ayarlarını doğru yapılandırmaktır. Android platformunda, özel bir ses ile bildirim gönderebilmek için yeni bir bildirim kanalı (channel) oluşturmanız gerekir. Bu kanal üzerinden gelen bildirimler, belirlediğiniz ses ile kullanıcıya ulaşacaktır. iOS platformunda ise bu tür bir ayara gerek yoktur; ses dosyasını doğrudan bildirim içeriğinde belirterek özel bir bildirim sesi iletebilirsiniz.

Bildirim ayarlarını aşağıdaki gibi tanımlayın:

Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: true,
shouldSetBadge: true,
}),
});

Ardından, App.js dosyanızda useEffect içinde konfigürasyonunuzu ayarlayın:

 if (Platform.OS === "android") {
await Notifications.setNotificationChannelAsync("bip", {
name: "BipChannel",
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
sound: "bip.mp3",
});
}

3. Bildirim Gönderme

Artık konfigürasyonumuz tamam, özel bildirimleri göndermeye hazırız! Özel sesi içeren bir test bildirimi göndermek için aşağıdaki fonksiyonu kullanabilirsiniz:

  const handleTestNotification = async () => {
if (Platform.OS === "android") {
await Notifications.scheduleNotificationAsync({
content: {
title: "Test Notification",
sound: true,
},
trigger: {
seconds: 1,
channelId: "bip",
},
});
} else {
await Notifications.scheduleNotificationAsync({
content: {
title: "Test Notification",
sound: "bip.mp3",
},
trigger: {
seconds: 1,
},
});
}
};

Not: Android için sound: true ve channelId: "bip", iOS için ise sound: "bip.mp3" ayarlarını eklemeyi unutmayın.

Expo ile özel bildirim sesleri kullanmak, mobil uygulamanızın kullanıcı deneyimini kişiselleştirmenin etkili bir yoludur. Android ve iOS için küçük farklılıklar içerse de, bu adımları takip ederek projelerinize hızlıca özel sesler ekleyebilirsiniz. Bu, uygulamanıza özgün bir dokunuş katacak ve kullanıcılarınız için daha unutulmaz bir deneyim sunacaktır.

Bu adımları izleyerek projelerinizde özel sesli bildirimlerin nasıl çalıştığını görmek için hemen bir test bildirimi gönderin!

İyi çalışmalar!

Kaynaklar

Expo Bildirim Dökümantasyonu

Apache Ignite

· 5 dakikalık okuma
Ahmet Buğra Kösen
Software Developer

Veritabanlarının ölçeklendirilmesi ve bellek içi veri işleme ihtiyaçları arttıkça, bu ihtiyaçları karşılayabilecek güçlü bir araç arayışı da ortaya çıkmaktadır. Apache Ignite, bu ihtiyaca çözüm sunan açık kaynaklı ve dağıtık bir bellek içi veri platformudur. Bu makalede, Apache Ignite'ı tanıyacak, onun temel özelliklerine göz atacak ve .NET Core ile nasıl entegre edileceğini örnek bir proje üzerinden göreceğiz.

Apache Ignite Nedir?

Apache Ignite, veri saklama ve işleme için bellek içi teknolojiler kullanan, ölçeklenebilir ve yüksek performanslı bir veri platformudur. Ignite, sadece verileri bellek içinde depolamakla kalmaz, aynı zamanda dağıtık veri işleme, SQL ve NoSQL veri yönetimi, data grid ve çok daha fazlasını sunar. Bu özellikleri sayesinde, düşük gecikmeli ve yüksek performanslı uygulamalar geliştirmenize olanak tanır.

Ignite, ölçeklendirme sorunları yaşayan veritabanları veya yoğun veri işleme ihtiyaçlarına çözüm getirmek için yaygın olarak tercih edilen bir araçtır. Özellikle büyük veri ve gerçek zamanlı işleme ihtiyaçları için ideal bir platformdur.

Apache Ignite'in Temel Özellikleri

  • Bellek İçi Depolama: Ignite, verilerinizi bellek içinde saklayarak yüksek hızda erişim sağlar.
  • Dağıtık SQL: Verilere SQL sorguları ile erişebilirsiniz, üstelik dağıtık bir ortamda.
  • Yatay Ölçeklendirme: Ignite, düğüm ekleyerek kolayca yatay ölçeklenebilir.
  • Düşük Gecikme Süresi: Ignite, verileri doğrudan bellekte tuttuğu için milisaniye mertebesinde gecikme süreleri sunar.

.NET Core ile Apache Ignite Entegrasyonu

Apache Ignite, .NET Core desteği sayesinde, .NET uygulamalarınızda bellek içi veri saklama ve dağıtık veri işleme yeteneklerini kullanmanıza olanak tanır. Ignite'ı .NET Core ile nasıl kullanabileceğimize bir bakalım.

Adım 1: Ignite Kütüphanesini Projeye Dahil Etmek

Öncelikle, Ignite'ın .NET Core desteği için gerekli olan NuGet paketini yükleyerek başlayacağız. Aşağıdaki komutu kullanarak bu paketi yükleyebilirsiniz:

Install-Package Apache.Ignite

Adım 2: Ignite Sunucusunu Başlatmak

Ignite sunucusunu başlatmak için basit bir konsol uygulaması oluşturacağız. Ignite sunucusu, verilerinizi depolamak ve dağıtık işlem yapmak için kullanacağımız ana bileşendir. Aşağıdaki kod parçacığı, Ignite sunucusunu başlatmak için kullanabileceğiniz bir örnek gösteriyor:

using Apache.Ignite;
using Apache.Ignite.Core.Cache.Configuration;
using System;

namespace IgniteDotNetExample
{
class Program
{
static void Main(string[] args)
{
var igniteConfiguration = new IgniteConfiguration
{
IgniteInstanceName = "myIgniteInstance",
WorkDirectory = "./igniteWorkDir",
ClientMode = false // Sunucu düğümü olarak çalıştırıyoruz.
};

IIgnite ignite = Ignition.Start(igniteConfiguration);

Console.WriteLine("Ignite sunucusu başlatıldı.");

// Örnek bir cache oluşturalım.
var cacheConfiguration = new CacheConfiguration
{
Name = "sampleCache",
CacheMode = CacheMode.Partitioned,
AtomicityMode = CacheAtomicityMode.Transactional
};

var cache = ignite.GetOrCreateCache<int, string>(cacheConfiguration);

// Cache'e veri ekleyelim.
cache.Put(1, "Merhaba Ignite!");
string value = cache.Get(1);

Console.WriteLine($"Cache'teki veri: {value}");

Console.ReadLine();
}
}
}

Yukarıdaki kodda, Ignite sunucusu başlatılmakta ve sampleCache isimli bir cache oluşturulmaktadır. Bu cache, verilerimizi bellek içinde saklamak için kullanılacaktır.

Adım 3: İki Ignite Instance ve Persistent Çalışma Yapısı

Apache Ignite'ın iki instance ile nasıl çalıştığını ve verilerin kalıcı olarak saklanmasını (persistent storage) nasıl sağlayacağınızı gösteren bir örnek hazırlayalım. Bu örnekte iki Ignite düğümü başlatacak ve persistent yapılandırma ile verilerin disk üzerinde saklanmasını sağlayacağız.

using Apache.Ignite.Core;
using Apache.Ignite.Core.Cache.Configuration;
using Apache.Ignite.Core.Configuration;
using System;

namespace IgnitePersistentExample
{
class Program
{
static void Main(string[] args)
{
// İlk Ignite instance'ını başlatma
var igniteConfig1 = new IgniteConfiguration
{
IgniteInstanceName = "igniteInstance1",
WorkDirectory = "./igniteWorkDir1",
DataStorageConfiguration = new DataStorageConfiguration
{
DefaultDataRegionConfiguration = new DataRegionConfiguration
{
Name = "Default_Region",
PersistenceEnabled = true // Kalıcı depolama etkinleştirildi.
}
}
};

IIgnite ignite1 = Ignition.Start(igniteConfig1);
ignite1.GetCluster().Active(true);
Console.WriteLine("Ignite Instance 1 başlatıldı.");

// İkinci Ignite instance'ını başlatma
var igniteConfig2 = new IgniteConfiguration
{
IgniteInstanceName = "igniteInstance2",
WorkDirectory = "./igniteWorkDir2",
DataStorageConfiguration = new DataStorageConfiguration
{
DefaultDataRegionConfiguration = new DataRegionConfiguration
{
Name = "Default_Region",
PersistenceEnabled = true // Kalıcı depolama etkinleştirildi.
}
}
};

IIgnite ignite2 = Ignition.Start(igniteConfig2);
ignite2.GetCluster().Active(true);
Console.WriteLine("Ignite Instance 2 başlatıldı.");

// Cache oluşturma ve veri ekleme
var cacheConfiguration = new CacheConfiguration
{
Name = "persistentCache",
CacheMode = CacheMode.Partitioned,
AtomicityMode = CacheAtomicityMode.Transactional
};

var cache = ignite1.GetOrCreateCache<int, string>(cacheConfiguration);
cache.Put(1, "Persistent Merhaba Ignite!");
string value = cache.Get(1);

Console.WriteLine($"Persistent Cache'teki veri: {value}");

Console.ReadLine();
}
}
}

Yukarıdaki kodda, iki farklı Ignite instance'ı başlatılmakta ve kalıcı depolama (persistent storage) yapılandırması etkinleştirilmektedir. Bu sayede Ignite sunucuları yeniden başlatılsa bile veriler disk üzerinde saklanacak ve kaybolmayacaktır.

Adım 4: Ignite ile Verilere Erişmek

Ignite'ın en güçlü özelliklerinden biri SQL desteğidir. Ignite üzerinde saklanan verileri SQL sorguları kullanarak işleyebilirsiniz. Örneğin, bir kullanıcı tablosunu Ignite üzerinde saklamak ve SQL sorguları ile bu tablodaki verilere erişmek mümkündür:

[Serializable]
public class User
{
[QuerySqlField(IsIndexed = true)]
public int Id { get; set; }

[QuerySqlField]
public string Name { get; set; }
}

class Program
{
static void Main(string[] args)
{
IIgnite ignite = Ignition.Start();

var cache = ignite.GetOrCreateCache<int, User>(new CacheConfiguration("userCache", typeof(User)));

// Kullanıcı ekleyelim.
cache.Put(1, new User { Id = 1, Name = "Ali" });
cache.Put(2, new User { Id = 2, Name = "Ayşe" });

// SQL sorgusu ile kullanıcıları getirelim.
var query = new SqlFieldsQuery("SELECT Id, Name FROM User WHERE Name = ?", "Ali");
var cursor = cache.Query(query);

foreach (var row in cursor)
{
Console.WriteLine($"Kullanıcı: Id={row[0]}, Name={row[1]}");
}

Console.ReadLine();
}
}

Yukarıdaki örnekte, User isimli bir sınıf oluşturduk ve bu sınıfı userCache isimli bir cache içinde sakladık. Daha sonra, SQL sorguları kullanarak bu cache içinde saklanan verilere eriştik.

Sonuç

Apache Ignite, ölçeklenebilir ve yüksek performanslı bellek içi veri yönetimi ve işleme çözümleri sunan güçlü bir platformdur. Ignite, verileri bellek içinde saklayarak ve dağıtık SQL desteği sunarak, .NET uygulamalarınızı hızlandırmanıza ve verimliliğini artırmanıza yardımcı olabilir. Bu makalede, Apache Ignite'ın temel özelliklerini ve .NET Core ile nasıl entegre edilebileceğini örneklerle ele aldık. Ayrıca, iki Ignite instance'ı kullanarak kalıcı depolama özellikleriyle çalışan bir yapı kurmayı öğrendik.

Ignite'ın sunduğu diğer güçlü özellikleri keşfetmek ve daha fazla bilgi edinmek için Apache Ignite resmi dokümantasyonuna göz atabilirsiniz.

Yorumlar ve Katkılar

Apache Ignite ve .NET Core ile ilgili deneyimlerinizi paylaşmak veya sorular sormak için yorum yapmaktan çekinmeyin. Apache Ignite ile gerçek zamanlı veri işleme uygulamaları geliştirmek konusunda daha fazla bilgi edinmek isteyenler için Ignite'ın güçlü özellikleri gerçekten keşfetmeye değer.

Mutation Testing

· 4 dakikalık okuma
Ahmet Buğra Kösen
Software Developer

x.png

Yazılım geliştirme süreçlerinde unit testler, kod kalitesini ve güvenilirliğini artırmak için vazgeçilmez bir araçtır. Ancak, unit testlerimizin gerçekten etkili olup olmadığını nasıl anlayabiliriz? İşte bu noktada Mutation Testing devreye giriyor. Bu makalede, Mutation Testing kavramını, manuel olarak nasıl uygulanabileceğini ve .NET ekosisteminde popüler bir araç olan Stryker.NET ile nasıl otomatize edilebileceğini inceleyeceğiz.


Mutation Testing Nedir?

Mutation Testing, testlerinizin etkinliğini değerlendirmek için kullanılan bir tekniktir. Bu yöntemde, kodunuzda küçük değişiklikler (mutasyonlar) yapılarak testlerinizin bu değişiklikleri yakalayıp yakalayamadığı kontrol edilir. Eğer testleriniz bu mutasyonları tespit edemezse, bu durum test senaryolarınızın geliştirilmesi gerektiğini gösterir.

Neden Önemlidir?

  • Test Kalitesini Artırır: Sadece kodun test edilip edilmediğini değil, testlerin ne kadar etkili olduğunu da ölçer.
  • Hata Yakalama Yeteneğini Geliştirir: Potansiyel hataları erken aşamada tespit etmeyi sağlar.
  • Güvenilirlik Sağlar: Kodunuzun değişikliklere karşı ne kadar dayanıklı olduğunu gösterir.

Manuel Olarak Mutation Testing Nasıl Yapılır?

Otomatik araçlar kullanmadan da mutation testing prensiplerini uygulayabilirsiniz. Bu bölümde, manuel olarak nasıl mutation testing yapabileceğinizi basit bir örnekle göstereceğiz.

Öncelikle, test etmek istediğimiz basit bir sınıf ve buna ait unit testleri yazalım.

MathOperations.cs:

namespace MutationDemo;

public class MathOperations
{
public int Add(int a, int b) => a + b;
}

Unit Test:

using Xunit;
using FluentAssertions;

namespace MutationDemo.UnitTests;

public class MathOperationsTests
{
[Fact]
public void Add_ShouldReturnCorrectSum()
{
// Arrange
var mathOperations = new MathOperations();

// Act
var result = mathOperations.Add(2, 3);

// Assert
result.Should().Be(5);
}
}

Test projesinin bulunduğu dizinde dotnet test komutu ile yazdığımız testi çalıştırdığımızda testimizin başarıyla geçtiğini göreceğiz;

Passed!  - Failed:     0, Passed:     1, Skipped:     0, Total:     1, Duration: < 1 ms

Şimdi, kodumuzda bilinçli bir hata yaparak mutasyon oluşturalım. Örneğin, + operatörünü - operatörüyle değiştirelim:

public int Add(int a, int b) => a - b;

Değişiklikleri kaydettikten sonra dotnet test komutu ile testi tekrar çalıştıralım: Test çıktısı aşağıdaki şekilde olmalı:

Failed!  - Failed:     1, Passed:     0, Skipped:     0, Total:     1, Duration: < 1 m

Testin başarısız olması, testimizin bu mutasyonu yakaladığını gösterir. Süper bir unit test yazmışız, testimiz koddaki bu hatayı tespit edebiliyor.


Diğer olası mutasyonları da deneyebilirsiniz. Örneğin, return a + b; satırını return a; olarak değiştirebiliriz:

public int Add(int a, int b) => a;

Testleri tekrar çalıştırdığınızda, testin yine başarısız olması gerekir. Eğer testler başarılı olursa, bu durum testlerinizin yeterince kapsamlı olmadığını gösterir ve testlerinizi gözden geçirmeniz gerekir.


Stryker.NET Nedir?

Manuel olarak mutation testing yapmak, küçük projelerde uygulanabilir olsa da, büyük projelerde zaman alıcı ve karmaşık olabilir. İşte bu noktada Stryker.NET devreye giriyor. Stryker.NET, .NET platformu için geliştirilmiş açık kaynaklı bir mutation testing aracıdır. Kodunuzda otomatik olarak mutasyonlar oluşturarak, testlerinizin bu mutasyonları yakalayıp yakalayamadığını analiz eder.

Özellikleri

  • Kolay Entegrasyon: Mevcut .NET projelerinize hızlıca entegre edilebilir.
  • Esnek Yapılandırma: Farklı test çerçeveleri (xUnit, NUnit, MSTest) ile uyumludur.
  • Detaylı Raporlama: Mutasyon skorları ve hangi mutasyonların tespit edilemediği gibi detaylı raporlar sunar.

Stryker.NET ile Mutation Testing

Biraz önce manuel olarak gerçekleştirdiğimiz mutasyon testini, Stryker.NET'i kullanarak aynı projede otomatikleştirelim.

Gereksinimler

  • .NET 6 veya daha yeni bir sürüm

  • Unit testler için xUnit

  • Stryker.NET'in kurulu olması

    Stryker.NET'i global bir araç olarak kurmak için komut satırında aşağıdaki komutu çalıştırın;

    dotnet tool install -g dotnet-stryker

Stryker.NET ile Mutation Testing'i Çalıştırma

Test projenizin dizininde aşağıdaki komutu çalıştırın:

dotnet stryker

Bu komut, Stryker.NET'in varsayılan ayarlarla mutation testing'i başlatmasını sağlar. Testler tamamlandıktan sonra Stryker.NET size bir rapor sunacaktır.

image.png

Stryker tarafından üretilen html raporunu incelediğimizde Stryker'ın kodun hangi kısmında kaç tane ve nasıl mutasyon oluşturduğunu görebiliriz;

image.png

Mutasyon skorunun %100 olduğunu görüyoruz. Yani bu, testlerimizi Add methodunda yaptığımız değişiklikleri kapsayacak şekilde yazdığımız anlamına geliyor.

Stryker.NET, hangi mutasyonların öldürüldüğünü (testler tarafından yakalandığını) ve hangilerinin hayatta kaldığını (testlerin yakalayamadığını) detaylı bir şekilde raporlar. Bu raporları inceleyerek, testlerinizde hangi senaryoların eksik olduğunu tespit edebilirsiniz.


Sonuç

Mutation Testing, unit testlerinizin gerçekten etkili olup olmadığını anlamak için güçlü bir yöntemdir. Manuel olarak uygulanabilir olsa da, Stryker.NET gibi araçlar sayesinde bu süreci otomatikleştirerek zaman ve emek tasarrufu sağlayabilirsiniz. Bu sayede kod kalitenizi artırabilir ve olası hataları erken aşamada tespit edebilirsiniz.

Dipnot: Emek emek yazdığım 2000’e yakın test barındıran canım kütüphanemde, mutasyon skorunun %40 olduğunu görünce yaşadığım hüsranı size tarif edemem 😟 Sizde aynı hüsranı yaşamak istemiyorsanız test yazım tekniklerinizi mutasyon raporlarını inceleyerek iyileştirebilirsiniz.

Bir sonraki yazıda görüşmek dileğiyle…

Source Control Standartları

· 16 dakikalık okuma
Ahmet Buğra Kösen
Software Developer

project-history 1.png

Ekipler büyüdükçe artık belirli standartları uygulamak zorunlu hale gelmeye başlar. Aksi takdirde projelerin veya kaynak kodların yönetimini sağlamak ve çalışma ortamının verimliliğini sürdürmek zorlaşır.

Birazdan anlatacağımız standartlar için gelebilecek; "neden yapıyoruz ki?" veya "o kadar işin arasında birde bu vakit kaybı değil mi?" gibi sorularının genel bir cevabını vererek giriş yaptığımıza göre, bu dökümanda neleri ele alacağımıza bir göz atalım;

  • Semantic Versioning
  • Perfect Commit;
    • Perfect Commit Messages
    • Conventional Commits
  • Branch Naming

Semantic Versioning(SemVer)

Source Control Standarts isimli bir dökümanda neden versioningle ilgili bir bölüm olduğunu merak ediyorsan sabırsızlanma ve okumaya devam et 😉

Semantic Versioning Nedir?

Semantic versioning (semantik sürümleme), yazılım projelerinde sürüm numaralarını belirlemenin standart bir yoludur. Bu standart, sürüm numaralarının anlamlı ve öngörülebilir olmasını sağlar. Semantic versioning, genellikle MAJOR.MINOR.PATCH formatında kullanılır.

Her bölümün ne anlama geldiğine ve ne zaman artırılacağına bir göz atalım:

  1. MAJOR (Ana Sürüm): Geriye dönük uyumluluğu bozan değişiklikler yapıldığında artırılır.
  2. MINOR (Alt Sürüm): Geriye dönük uyumlu yeni özellikler eklendiğinde artırılır.
  3. PATCH (Yama): Geriye dönük uyumlu hata düzeltmeleri yapıldığında artırılır.

Frame 1.png

Neden Semantic Versioning Kullanmalısın?

  • Anlaşılabilirlik: Sürüm numarasından yazılımın ne kadar değiştiğini ve bu değişikliklerin uyumluluk üzerinde ne tür etkileri olduğunu kolayca anlayabilirsin.
  • Güvenilirlik: Yazılımı kullananlar, sürüm numarasına bakarak güncellemelerin risklerini daha iyi değerlendirebilirler.
  • İşbirliği: Takım arkadaşların ve kullanıcılar, sürüm numaralarının anlamını bildiklerinde, projeye katkı sağlama ve yazılımı kullanma konusunda daha güvenli hissederler.

Semantic Versioning Nasıl Yapılır?

Ekip olarak bir e-ticaret sitesi için API geliştirdiğimizi varsayalım. Aşağıda buna göre senaryolar ve bu senaryolarda sürüm numarasının nasıl değişeceği yer almakta:

  • API temel özelliklerini ekliyorsun ve henüz kullanıcıların kullanımına uygun değil, yani stable bir sürüme sahip değilsin. Bu durumda ilk sürüm numarası 0.1.0 şeklinde olmalı.
  • Kullanıcıların giriş yapmaları için gerekli endpointleri ekledin. Bu durumda bir sonraki sürüm numarası 0.2.0 olmalı.
  • Yeni eklediğin endpointlerde hatalar olduğunu farkettin ve bunları düzelttiğin bir sürüm yayınladın. Bu durumda sürüm numarası 0.2.1 olmalı.
  • API'ın temel özelliklerini tamamladın ve artık api kullanıcıların kullanabileceği bir hale geldi. Bu durumda ilk sürüm numaran 1.0.0 olmalı.
    • Stable bir sürüm yayınlamak, aslında son deneme sürümünü teslim etmek anlamına da gelir. Yani 0.2.1 ve 1.0.0 sürüm numaraları aslında aynı olabilir. Bu durumda geriye dönük uyumsuzluk beklenmez. Geriye dönük uyumsuzluklar genellikle 1.0.0'dan sonraki sürümlerde ortaya çıkar.
  • API'ına belirli özellikler ekledin ve şuanki sürüm numarası 1.17.4. API performans iyileştirmesi ve güvenlik zafiyetlerini gidermek amacıyla kullandığın framework ve paketlerin versiyonunu güncelledin ve buna bağlı olarak API'da artık geriye dönük uyumluluğu olmayan değişiklikler yaptın. Bu durumda bir sonraki sürüm numaran 2.0.0 olmalı.
  • İş birimin senden yeni bir ödeme altyapısı eklemeni istedi. Bu durumda yeni sürüm numaran 2.1.0 olmalı

Hayali e-ticaret sitemizi semantik bir şekilde versiyonladığımıza göre SONUÇ ;

Semantic versioning, projeni düzenli ve anlaşılır tutmanın bir standardıdır. Projenin her aşamasında, sürüm numaralarını doğru bir şekilde güncelleyerek projenin kullanıcılarına ve ekip arkadaşlarımıza projenin durumu hakkında daha net bilgiler verebiliriz. Bu standardı kullanarak, yazılım geliştirme sürecini daha yönetilebilir ve güvenilir hale getirebiliriz.

Semantic Versioning uygularken uyman gereken kurallar ve daha fazla bilgi için SemVer Resmi Sitesi'ne göz atabilirsin.


Perfect Commit Messages

Untitled

Her ne kadar önemsiz görülse de, commit mesajları yazılım geliştirme sürecinin önemli bir parçasıdır. İyi yazılmış bir commit mesajı, hem senin hem de takım arkadaşlarının projeyi daha iyi anlamasını sağlar. "Bu değişikliği kim neden yaptı abi?" gibi soruların tarih olmasını sağlayıp, özellikle sürekli farklı projelerde görev alan ekip üyeleri için ciddi bir zaman kazancı sağlar.

Mükemmel commit mesajını yazmak işin sadece yarısı, mükemmel commitleri oluşturmak için değişiklikleri parçalara bölmek ve bu parçalara neler eklenmesi gerektiğini planlamak da aynı şekilde önemlidir. Commit'ler bir task'ı ele alış biçiminde önemli bir rol oynayabilir. Değişiklikleri mantıksal olarak commit'lere gruplandırmak, bir task'ı planlama ve onu daha küçük parçalara bölerek yazılım geliştirme sürecini iyileştirmen için de olanak sağlar.

Bu, task'ın ve bu task'a ürettiğin çözüm hakkında daha fazla düşünmeni sağlayacak, sadece işin başında değil, aynı zamanda değişiklikleri commitlere parçalayıp bölerken ve commit mesajını yazarken de bunu yapmanı sağlayacak. Bu da, uygulamanı yeniden gözden geçirmeni ve belki de gözden kaçan edge caseleri, eksik testleri veya unuttuğun başka herhangi bir şey fark etmeni sağlayabilir.

Peki Nasıl?

Yaptığımız geliştirmeyi uygun şekilde commitlere parçalama işini sana bıraktığımıza göre, commit mesajını nasıl yazmalıyız sorusuna cevap verelim. Burada tutarlılık çok önemlidir, bu yüzden ekipler önce aşağıdaki üç konuda tartışmalı ve anlaşmalıdır;

  • Style: Commit geçmişinin okunabilir olmasını sağlamak için önemli bir rol oynar. Dil bilgisi, noktalama işaretleri, büyük harf kullanımı ve satır uzunlukları gibi konuları içerir.
  • Content: Content'i standardize etmek kolay değildir. Fakat yapılan değişikliklerin neden yapıldığı ve nasıl gerçekleştirildiği hakkında bilgiler ve gerektiğinde, yapılan değişikliklerin teknik detaylarını ve etkilerini içermelidir.
  • Metadata: Issue Tracking Id'leri, değişikliklerin test edilip edilmediğini belirten notlar veya gerekirse, kod inceleme sürecinde elde edilen bulgular veya yorumlar gibi ek bilgiler içermelidir.

Bu üç konuyu ele almanın tek bir yolu yoktur yani tartışmaya açıktır, ancak çoğu Git commit mesajı belirli bir deseni takip eder. Aşağıda bu yaygın kullanılan deseni inceleyeceğiz.

Şablon

[subject]

[optional body]

[optional footer(s)]

Subject (Başlık)

Tıpkı bir e-postada olduğu gibi, başlık çok önemli bir parçadır. Genellikle insanların okuyacağı ilk, belki de tek kısım burasıdır, bu yüzden görsel olarak çekici ve kolay anlaşılabilir olmalıdır, gereksiz büyük harf ve noktalama işaretlerinden kaçınmalı ve doğru anahtar kelimeler kullanılmalıdır.

Emir kipi standarttır; Git, senin adına bir commit oluşturduğunda (örneğin git merge veya git revert komutlarını çalıştırdığında) emir kipini kullanır. Bu, "Added" veya "Adds" yerine "Add" yazman gerektiği anlamına gelir. Başlıkta yazan metin, şu cümleyi tamamlamalıdır: "Eğer uygulanırsa, bu commit...". Çoğu ekip commit subject için aşağıdaki kuralları uygular;

  • Büyük harfle başlamalı
  • Nokta ile bitmemeli
  • 50 karakter veya daha kısa olmalı
    • Karakter sınırı elbette aşılabilir, ancak commit mesajının net ve sade olmasını sağlamak için bunu uygulamak iyi bir pratik olacaktır.

İyi bir commit başlığına örnek : Update configuration files with new staging URL

Tekrar edelim, bu kurallar "bunu yapamazsın, yaparsan taş olursun" gibi katı kurallar değildir. İstersen commit mesajlarına emoji bile ekleyebilirsin 😊

Body (Gövde)

Başlık çoğu zaman kendini açıklayıcıdır, ancak bazen "body" alanına daha fazla bilgi eklemek gerekebilir. Bu alanı, NEYİN ve NEDEN değiştiği hakkında daha fazla bağlam sağlamak için kullanırız.

Çoğu ekip commit body için aşağıdaki kuralları uygular;

  • Subject'ten ayırmak için bir boş satır kullanılmalı
  • Paragraflar boş satırlarla veya bullet list vb. stillerle düzenlenmeli
  • Satır uzunluğu 72 karakter veya daha kısa olmalı

"Bi dakika arkadaşım bu karakter sınırı neden 72" dediğini duyar gibiyim;

72 karakter sınırı, terminal genişliği ve e-posta istemcileri gibi sistemlerin metin kaydırmayı(wrapping) düzgün şekilde işleyebilmesi ve okunabilirlik gibi tarihsel ve pratik nedenlerden dolayı yaygın olarak kullanılır. Bu, eski donanımlar ve yazılımlar için önemli bir gereklilikti ve artık bunu uygulamamıza pek ihtiyaç yok gibi gözüküyor. Fakat bu sınır, mesajların daha düzenli ve anlaşılır olmasını sağlamak için günümüzde de bir standart olarak kullanılabilir. Özellikle open-source projelerde hala yaygın kullanılan bir konvansiyondur.

Subject alanındaki karakter sınırlamasına benzer şekilde, bu daha çok bir yönerge olmakla birlikte, katı bir kural değildir. Bunu uygulayıp uygulamamak daha çok commit mesajlarını sıklıkla görüntülediğimiz editörün yeteneklerine bağlı olarak değişmelidir.

Tim Pope'un aşağıdaki örneği, stil olarak ulaşmamız gereken hedefin bir örneği olarak gösterilebilir:

Short(50 chars or less) summary of changes

More detailed explanatory text, if necessary. Wrap it to 72 characters.
The blank line separating the summary from the body is critical (unless
you omit the body entirely); tools like rebase can get confused if you run
the two together.

Further paragraphs come after blank lines. Bullet points are okay, too.

- Use a hyphen or asterisk for bullet points.
- Capitalize the first letter of each point.

Metadata/Footer

Footer'a commit'in ilgili olduğu Azure DevOps taskları veya user storyleri, Pull Request'ler veya Jira ticket'larını ekleyebiliriz. Aynı zamanda bu alan, kullanımdan kaldırılan özelliklerin ve geriye dönük uyumsuz değişikliklerin belirtilmesi gereken yerdir. Örnek;

BREAKING CHANGE: <summary>
<blank line>
Fixes #<user story>
Closes #<pr>

"Bir commit mesajına bu kadarda uğraşmak overengineering be!" diye hayıflanan ve takımın standartlarını yakalayamamış arkadaşım bir önceki atttığın commit mesajını düzeltebileceğin bir yöntemde sana gelsin. Git'in --amend seçeneğini kullanarak çirkince yazdığın o commit mesajını düzenleyip hatandan geri dönebilirsin 😑

Şablonu parça parça inceledik. Hepsini bir araya getirdiğimizde, commitlerimiz aşağıdaki örnekteki gibi olmalıdır;

Add user authentication feature

- Implemented user authentication using JWT tokens for secure login.
- Added user registration functionality with password hashing for security.

Fixes #123
Closes #456
Not Tested

Conventional Commits

Untitled

Gelelim fasulyenin faydalarına 🫘

Bir önceki sectionda oluşturduğumuz perfect commit'in üzerine, Conventional Commit şartnamesinde belirlenen standartları uygulayarak, commit mesajının üstünde bir çatı oluşturmaya çalışıyoruz ki anlamlı bir commit geçmişi elde edip, bu geçmişten çeşitli raporlar elde edebilelim ve bize belli başlı yetenekler kazandırsın. Yani human-readable olan commit mesajlarımızı human & machine-readable hale getireceğiz. Ayrıca bu şartname Semantic Versioning ile uyumludur.

Şablon

<type>[optional scope]: <subject>

[optional body]

[optional footer(s)]

Conventional Commit, aşağıdaki yapısal unsurları içermelidir;

  1. fix: fix tipi bir commit kodunuzdaki bir hatayı düzeltir (Semantik versiyonlamadaki PATCH ile paraleldir).
  2. feat: feat tipi commit kodunuza yeni bir özellik ekler (Semantik versiyonlamadaki MINOR ile paraleldir).
  3. BREAKING CHANGE: BREAKING CHANGE: ile başlayan bir footer ya da type/scope sonuna eklenmiş bir ! içeren commit geriye dönük uyumluluğu olmayan bir değişiklik getiriyordur (Semantik versiyonlamadaki MAJOR ile paraleldir). Bir BREAKING CHANGE harhangi bir tip commit içinde olabilir.

Yukarıdakiler Conventional Commit şartnamesinde yazılı olan ve semantic versioning'e paralel olan tiplerdir. Bunlar dışında yaygın kullanılan fakat semantic versioning'e etkisi olmayan tipler ve kodumuza olan etkileri;

  1. docs: Sadece dökümantasyon ile ilgili değişiklikler yapar. Örneğin, README dosyasını güncellemek.
  2. style: Kodun stilini ve formatını değiştirir, ancak işlevini etkilemez. Örneğin, boşlukları düzenlemek veya satır sonu karakterlerini değiştirmek.
  3. refactor: Kodun işlevselliğini değiştirmeden yeniden düzenler. Örneğin, bir fonksiyonu daha okunabilir hale getirmek.
  4. perf: Performans iyileştirmeleri yapar. Örneğin, bir algoritmayı daha hızlı çalışacak şekilde optimize etmek.
  5. test: Test ekler veya mevcut testlerde değişiklik yapar. Örneğin, bir fonksiyonun yeni bir test senaryosunu eklemek.
  6. build: Build sistemini veya dış bağımlılıkları etkileyen değişiklikler Örneğin (örnek: nuget, npm)
  7. ci: Sürekli entegrasyon (CI) yapılandırmasında değişiklikler yapar. Örneğin, CI araçlarının yapılandırma dosyalarını güncellemek.
  8. chore: Diğer kategorilere uymayan değişiklikler yapar. Örneğin, .gitignore dosyasına yeni exclusionlar eklemek.
  9. revert: Önceki bir commit'i geri alır. Örneğin, yanlışlıkla yapılan bir değişikliği geri almak.

Örnekler

Subject ve geriye dönük uyumluluğu olmayan bir değişiklik içeren footer'a sahip bir commit mesajı;

feat: allow provided config object to extend other configs

BREAKING CHANGE: `extends` key in config file is now used for extending other config files

Geriye dönük uyumluluğu olmayan bir değişikliği! ile belirten bir commit mesajı;

feat!: send an email to the customer when a product is shipped

Kapsamı(scope) belirten ve geriye dönük uyumluluğu olmayan bir değişikliği ! ile belirten bir commit mesajı;

feat(api)!: send an email to the customer when a product is shipped

! içeren ve BREAKING CHANGE içeren footer'a sahip commit mesajı;

chore!: drop support for Node 6

BREAKING CHANGE: use JavaScript features not available in Node 6.

Body'si olmayan commit mesajı;

docs: correct spelling of CHANGELOG

Scope belirtilen commit mesajı;

feat(lang): add Polish language

Çok paragraflı bir body ve birden çok footer metadatası içeren commit mesajı;

fix: prevent racing of requests

Introduce a request id and a reference to latest request. Dismiss
incoming responses other than from latest request.

Remove timeouts which were used to mitigate the racing issue but are
obsolete now.

Reviewed-by: Z
Refs: #123

Şablonu ve içeriğini aklımızda canlandırabildiğimize göre dökümanı daha da uzatmamak adına, kalan detayları Conventional Commits Şartnamesi'nden okuyup öğrenmekte senin üzerine düşen görev. Şimdi diğer bölümlerde yaptığımız gibi neden kullanıyoruz sorusuna cevap verip bu bölümü sonlandıralım;

  • CHANGELOG'ları otomatik olarak human&machine-readable şekilde oluşturmamızı sağlar.

    Untitled

  • Kolayca semantik versiyonlama yapmamıza olanak sağlar.

  • Hangi projenin, hangi kapsamında, hangi türde geliştirmeler, iyileştirmeler, hata düzeltmeleri yapıldığı raporlanabilir.

  • Belirtilen tiplere göre otomasyon araçları geliştirmeye veya operasyonel işlemler yürütmeye (tip alanında "ci" görülen commit mesajlarının deployment pipeline'larını tetiklemesi, "test" görülenlerin birim veya uçtan uca testleri tetiklemesi vs. gibi.) imkânlar tanınıyor.

  • Kalabalık ekiplerde aynı dilin konuşulmasını, kişilerin, başkalarının yaptığı değişiklikleri çok daha kolay anlaması sağlanır.

  • Jira, Trello gibi proje yönetim araçlarındaki iş detaylarından spesifik olarak ilgili işin yaşam döngüsü izlenebilir.

  • İnsanların daha yapılandırılmış bir commit geçmişini kendi kendilerine keşfetmelerine imkan vererek projelerinize katkıda bulunmalarını kolaylaştırmak.

  • Çıktı olarak sürüm takibi tarihçeleri üzerine analiz yapabilecek araçlar için düzenli bir veri seti oluşturuyor.

Bonus


Branch Naming

Frame 2.png

Commitlerimizin bir standardı olur da commitlerimizi pushladığımız branchlerin olmaz mı? Tabi ki var!

Kod base'de yapacağımız değişiklikleri yapmadan önce hepimiz bir branch oluşturuyoruz. Bu branchlerin yönetimi bazı durumlarda zorlaşabiliyor. Bunun önüne geçmek için branchleri etkili bir şekilde adlandırmak ve organize etmek, geliştirme sürecinin verimliliğini artırabilir. Branch isimlendirme konusunda dikkate alınması gereken yaygın kullanılan standartları incelemeden önce Regular Branches adını verdiğimiz uzun ömürlü branchlere bir göz atalım;

Git'teki regular branches, devam eden çalışmaları organize etmek için stable ve structured bir yol sunan uzun ömürlü branchlerdir. Bu branchler, repoda kalıcı olarak bulunur ve isimlendirmeleri basittir. Bazı regular Git branchleri şunlardır;

  • Master (master/main) Branch: Git reposundaki varsayılan production branchidir ve sürekli olarak stable olması gerekir. Geliştiriciler, kod review ve testten sonra değişiklikleri master branchine birleştirebilirler. Projedeki tüm ekip arkadaşları, master branchini stable ve up-to-date tutmalıdır.
  • Development (dev) Branch: Ana geliştirme branchidir ve geliştiricilerin yeni özellikleri, hata düzeltmelerini ve diğer değişiklikleri entegre etmesi için bir merkez olarak hizmet eder. Ana amacı, geliştiricilerin değişiklikleri doğrudan master branchine uygulamaktan kaçınmalarını sağlamaktır. Geliştiriciler, değişiklikleri dev branchinde test eder, inceler ve daha sonra master branchine birleştirirler.
  • QA (QA/test) Branch: QA ve otomasyon testi için hazır olan tüm kodları içeren branchtir. Production ortamına herhangi bir değişiklik uygulamadan önce QA testleri gereklidir, böylece stable bir kod base oluşturulur.

Regular branchler şimdi bahsedeceğimiz isimlendirme kurallarını uygulamazlar.

Style

  • Küçük harf ve "-"(tire): Branch isimleri için küçük harfler ve kelimeleri ayırmak için tire kullanmalısın. Örneğin, feature/new-login veya bugfix/header-styling.
  • Alphanumeric karakterler: Yalnızca alphanumeric karakterler (a-z, 0–9) ve tireler kullanmalısın. Noktalama işaretleri, boşluklar, alt çizgiler veya alphanumeric olmayan herhangi bir karakter kullanımından kaçınmalısın.
  • Ardışık tireler: Ardışık tirelerden kaçınmalısın. Örneğin feature--new-login karışık ve okunması zor olabilir.
  • Tire ile bitiş: Branch adını tire ile bitirmemelisin. Örneğin, feature-new-login- şeklinde bir branch ismi oluşturmamalısın.
  • Açıklayıcı: Yaptığın isimlendirme açıklayıcı ve öz olmalıdır, ideal olarak branch'te yapılan çalışmayı yansıtmalıdır.

Branch Prefix

Branchlerin amaçlarını hızlıca tanımlamak için prefixlerin kullanılması, branch türlerinin tanımlanmasına yardımcı olur;

  • Feature Branch: Bu branchler yeni özellikler geliştirmek için kullanılır. Örneğin, feature/login-system
  • Bugfix Branch: Kod içindeki hataları düzeltmek için kullanılır. Örneğin, bugfix/header-styling
  • Hotfix Branch: Production ortamındaki kritik hataları düzeltmek için doğrudan production branchinden oluşturulur. Örneğin, hotfix/critical-security-issue
  • Release Branch: Yeni bir production sürümü için hazırlık yapmak için kullanılır. Örneğin, release/v1.0.1
  • Documentation Branch: Döküman yazmak, güncellemek veya düzeltmek için kullanılır. Örneğin, docs/api-endpoints
  • Experimental Branch: Bir release'in veya sprintin parçası olmayan yeni ve henüz test edilmemiş özellikler veya fikirler geliştirmek için kullanılır. Bu branchler, deneme amaçlıdır ve mevcut projede uygulanıp uygulanmayacağı belli değildir. Örneğin; experimental/new-order-process-algorithm
  • WIP Branch: Geliştiricilerin üzerinde çalıştıkları, ancak henüz tamamlanmamış olan özellikler veya görevler için kullanılır. Düzenli geliştirme sürecinin bir parçası olabilir veya olmayabilir. Genellikle belirli bir görev veya özellik üzerinde çalışırken kullanılır. Örneğin; wip/refactor-auth-system
  • Merging Branch: Merge conflict'leri ****çözmek veya iki farklı branch'i birleştirmek için geçici olarak kullanılır. Örneğin; merge/feature-login-with-dev

Bazı iş akışlarında, özellikle daha büyük ekiplerde, Jira, Azure DevOps gibi bir proje yönetimi aracından gelen ticket/task numarasını branch adına dahil etmek yaygındır. Bu, belirli bir ticket/task için yapılan işin takibini kolaylaştırır. Örneğin, iş biriminiz kullanıcıların google ile login olabilmesini istedi. Bu özelliği eklemek için "T-123" numaralı bir jira ticket'ı açıldı, bu durumda branch adı feature/T-123-new-login-system olabilir. Konuya örnek olması açısından örnek branch isimleri;

  • Bir hata düzeltmesi için elinizde bir jira var ise örnek branch adı bugfix/EMJ-1789-fix-header-styling olmalı
  • Yeni bir geliştirme için elinizde bir User Story var ise örnek branch adı feature/US-1288-new-login-system olmalı
  • Yeni bir geliştirme için elinizde bir Task var ise örnek branch adı feature/T-1289-new-login-system olmalı

Git'te branch isimlendirme kuralları, sistem tarafından zorunlu tutulmasa da, özellikle bir ekip içinde çalışırken temiz ve anlaşılır bir kod base sağlamak için önemlidir. Bu kurallara uyarak, branchlerin kolayca anlaşılabilir olmasını sağlayabiliriz.


Yukarıda anlatılanları uygulayıp uygulamamak veya iş yoğunluğundan dolayı buna ayıracak vaktimiz yok gibi cümlelerle bunları ertelemek sana kalmış. Fakat Robert C. Martin'in Clean Code kitabında atıfta bulunduğu LeBlanc yasasının "Sonra asladır. (Later equals never.)" varsayımını unutmamak gerekiyor 🙂


Kaynaklar;