Cron İfadeleri: Unix 5 Alan, Saniyeli 6 Alan, Quartz Formatı ve Time Zone/DST Tuzakları
Unix cron 5 alanlıdır, Quartz saniyeyle başlayan 6–7 alan ister ve haftanın günlerini 1=pazar diye sayar; Quartz 4 "0 0 * * *"i varsayılan parser'la reddeder, CronFormat.Unix ile kabul eder. DST geçişinde 02:30'a kurulu günlük iş ilkbaharda 03:00'te bir kez, sonbaharda yalnız bir kez çalışır; aralıklı işler tekrarlanan saatte iki kez çalışır. Kendi cron scheduler'ını TimeProvider ile yazıp FakeTimeProvider ile test edebilirsin.
Sürüm notu: Bu yazıdaki kodlar .NET 10, Cronos 0.13.0 ve Quartz.NET 4.7.2 ile derlenip denenmiştir; test projesinde xUnit 2.9.3 ve Microsoft.Extensions.TimeProvider.Testing 10.11.0 kullanıldı.
Önce Tahmin Et
Bookstore'un Almanya'daki deposu için gece stok raporu her gün 02:30'da, Berlin saatiyle çalışıyor: 30 2 * * *. 29 Mart 2026'da saatler 02:00'den 03:00'e atlıyor; 25 Ekim 2026'da ise 03:00'ten 02:00'ye geri dönüyor ve 02:00–03:00 arası iki kez yaşanıyor. Rapor 29 Mart'ta çalışır mı, çalışırsa saat kaçta? 25 Ekim'de bir kez mi çalışır, iki kez mi? Peki her 30 dakikada bir çalışan */30 * * * * işi 25 Ekim gecesi kaç kez çalışır?
Bir de şu: aynı ifadeyi Quartz.NET'e 0 0 * * * olarak verirsen ne olur?
Unix Cron'u Alan Alan Okumak
Klasik Unix cron ifadesi beş alandan oluşur: dakika, saat, ayın günü, ay, haftanın günü. 30 2 * * * "her ayın her günü, haftanın her günü, saat 2, dakika 30" demek. Her alanda dört temel araç var: * her değer, , liste (1,15), - aralık (9-17) ve / adım (*/15, yani 0, 15, 30, 45). Haftanın günü 0–6 arasıdır, 0 pazar, 1 pazartesi; Vixie Cron geleneğinde 7 de pazar kabul edilir.
Okurken tek bir kurala dikkat: ayın günü ve haftanın günü alanlarının ikisi birden kısıtlanmışsa, klasik cron bunları "ve" ile değil "veya" ile birleştirir. 0 9 1 * 1 "her ayın 1'i pazartesiye denk geldiğinde" değil, "her ayın 1'inde ve her pazartesi" demektir. Quartz bu belirsizliği ? karakteriyle çözüyor; birazdan geleceğiz.
Hangfire, recurring job'larında aynı ekibin yazdığı Cronos kütüphanesini kullanıyor (Hangfire.Core'un içine gömülü olarak); Cronos'u kendi kodunda doğrudan da kullanabilirsin. Cronos standart 5 alanı, isteğe bağlı saniye alanını ve standart dışı L, W, # karakterlerini destekliyor.
Saniyeli 6 Alan ve Quartz Formatı
Saniye hassasiyeti gerektiğinde ifadenin başına bir saniye alanı eklenir: */30 * * * * * her 30 saniyede bir. Cronos bunu ayrı bir format olarak ister; aynı ifadeyi standart formatla parse etmeye çalışınca hata mesajı yanıltıcı olabiliyor, çünkü Cronos ilk alanı dakika sanıp kaydırarak okuyor:
try { CronExpression.Parse("0 0 0 * * *"); }
catch (CronFormatException ex) { Console.WriteLine($"Cronos '0 0 0 * * *' (Standard) -> {ex.Message}"); }
// Cronos '0 0 0 * * *' (Standard) -> The given cron expression has an invalid format. Days of month: Value must be a number between 1 and 31 (all inclusive).
Quartz formatı her zaman saniyeyle başlar ve 6 ya da 7 alandır: saniye, dakika, saat, ayın günü, ay, haftanın günü ve isteğe bağlı yıl. Unix'ten üç önemli farkı var. Birincisi ?: ayın günü ve haftanın günü alanlarından biri "belirtilmedi" anlamına gelen ? olur. İkincisi özel karakterler: L son (ayın son günü ya da 6L ile ayın son cuması), W en yakın iş günü (15W), # ayın n'inci haftanın günü (3#2, ayın ikinci salısı). Üçüncüsü ve en tehlikelisi: Quartz haftanın günlerini 1–7 arasında sayar ve 1 pazardır.
Quartz.NET 4 cron parser'ını sıkılaştırdı ve yanına bir Roslyn analyzer'ı ekledi. 5 alanlı bir Unix ifadesini sabit string olarak yazınca kod derlenmiyor:
var q = new Quartz.CronExpression("0 0 * * *");
// Program.cs(1,35): error QZ0001: '0 0 * * *' is not a valid cron expression: Cron expression '0 0 * * *' has 5 fields, which is the Unix/crontab form; Quartz cron has 6 or 7 fields with seconds first. Prepend a seconds field: "0 0 0 * * *". Note that Quartz numbers days of week 1-7 starting at Sunday where Unix cron uses 0-6, so respell numeric day-of-week values or use names (SUN, MON, ...). Or read it as written, renumbering and all, with CronExpression.Parse(expression, CronFormat.Unix).
Çalışma zamanında, örneğin configuration'dan gelen bir ifadeyle, aynı kontrol FormatException olarak geliyor. Mesajın son cümlesi önemli: Quartz 4, Unix formatını CronFormat.Unix ile açıkça istersen kabul ediyor ve haftanın günü numaralarını da Unix'e göre yorumluyor. Farkı gerçek bir koşuyla gördüm:
var baseTime = new DateTimeOffset(2026, 11, 8, 0, 0, 0, TimeSpan.Zero); // pazar
Console.WriteLine($"Unix '0 8 * * 1' -> {CronExpression.Parse("0 8 * * 1").GetNextOccurrence(baseTime.UtcDateTime):ddd yyyy-MM-dd HH:mm}");
Console.WriteLine($"Quartz '0 0 8 ? * 1' -> {QuartzCron.Parse("0 0 8 ? * 1").WithTimeZone(TimeZoneInfo.Utc).GetNextValidTimeAfter(baseTime):ddd yyyy-MM-dd HH:mm}");
Console.WriteLine($"Quartz Unix-format '0 8 * * 1' -> {QuartzCron.Parse("0 8 * * 1", Quartz.CronFormat.Unix).WithTimeZone(TimeZoneInfo.Utc).GetNextValidTimeAfter(baseTime):ddd yyyy-MM-dd HH:mm}");
// Unix '0 8 * * 1' -> Mon 2026-11-09 08:00
// Quartz '0 0 8 ? * 1' -> Sun 2026-11-08 08:00
// Quartz Unix-format '0 8 * * 1' -> Mon 2026-11-09 08:00
Aynı 1, Unix'te pazartesi (9 Kasım), Quartz formatında pazar (8 Kasım). Quartz'a Unix ifadesi verdiğinde ya CronFormat.Unix kullan ya da sayı yerine MON, SUN gibi isimler yaz; isimler iki formatta da aynı anlama geliyor. Ayrıca dikkat: Quartz 4.7.2 0 0 12 * * MON gibi iki gün alanını birden dolduran bir ifadeyi de kabul etti. Kesinlik için yine de birini ? yap.
Sık Kullanılan İfadelerin Üç Formatta Karşılığı
Aşağıdaki tabloyu ezbere yazmadım; her satırı hem Cronos'la hem Quartz 4.7.2 ile parse edip 8 Kasım 2026 00:00 UTC'den (Coordinated Universal Time) sonraki ilk çalışma zamanını hesaplattım ve dört hesabın aynı sonucu verdiğini kontrol ettim.
| Anlamı | Unix / Cronos | Cronos, saniyeli | Quartz |
|---|---|---|---|
| Her dakika | * * * * * | 0 * * * * * | 0 * * ? * * |
| Her 5 dakikada bir | */5 * * * * | 0 */5 * * * * | 0 0/5 * ? * * |
| Her 30 saniyede bir | yok | */30 * * * * * | 0/30 * * ? * * |
| Her saat :15'te | 15 * * * * | 0 15 * * * * | 0 15 * ? * * |
| Her gün 02:30 | 30 2 * * * | 0 30 2 * * * | 0 30 2 * * ? |
| Hafta içi 09:00 | 0 9 * * 1-5 | 0 0 9 * * 1-5 | 0 0 9 ? * MON-FRI |
| Hafta içi 09–17 arası 15 dakikada bir | */15 9-17 * * 1-5 | 0 */15 9-17 * * 1-5 | 0 0/15 9-17 ? * MON-FRI |
| Pazartesi 08:00 | 0 8 * * 1 | 0 0 8 * * 1 | 0 0 8 ? * 2 |
| Ayın 1'i 00:00 | 0 0 1 * * | 0 0 0 1 * * | 0 0 0 1 * ? |
| Ayın son günü 23:00 | 0 23 L * * | 0 0 23 L * * | 0 0 23 L * ? |
| Ayın son cuması 18:00 | 0 18 * * 5L | 0 0 18 * * 5L | 0 0 18 ? * 6L |
| Ayın ikinci salısı 10:00 | 0 10 * * 2#2 | 0 0 10 * * 2#2 | 0 0 10 ? * 3#2 |
| Ayın 15'ine en yakın iş günü 09:00 | 0 9 15W * * | 0 0 9 15W * * | 0 0 9 15W * ? |
Son dört satırdaki L, # ve W, Unix cron'un standart parçası değil; "Unix / Cronos" sütunundaki karşılıkları Cronos'un ve Quartz'ın CronFormat.Unix modunun kabul ettiği uzantılar. Doğrulama programının ilgili satırları:
mondays 08:00 cronos5=11-09 08:00:00 cronos6=11-09 08:00:00 quartz=11-09 08:00:00 quartzUnix=11-09 08:00:00
last day of month 23:00 cronos5=11-30 23:00:00 cronos6=11-30 23:00:00 quartz=11-30 23:00:00 quartzUnix=11-30 23:00:00
last Friday 18:00 cronos5=11-27 18:00:00 cronos6=11-27 18:00:00 quartz=11-27 18:00:00 quartzUnix=11-27 18:00:00
2nd Tuesday 10:00 cronos5=11-10 10:00:00 cronos6=11-10 10:00:00 quartz=11-10 10:00:00 quartzUnix=11-10 10:00:00
weekday nearest the 15th 09:00 cronos5=11-16 09:00:00 cronos6=11-16 09:00:00 quartz=11-16 09:00:00 quartzUnix=11-16 09:00:00
Dikkat ettiysen "pazartesi 08:00" satırında Quartz sütunu 2; Quartz'ın 1=pazar numaralandırmasının sonucu. Hafta günlerini Quartz'ta isimle yazmanın ikinci bir sebebi.
Time Zone Seçimi: UTC mi, Europe/Istanbul mu?
Cron ifadesi tek başına bir anı değil, bir duvar saati kuralını tarif eder. "Her gün 02:30" hangi saatin 02:30'u? Kütüphaneler varsayılan olarak farklı cevaplar veriyor; Hangfire recurring job'larda UTC'yi varsayılan alır ve RecurringJobOptions.TimeZone ile değiştirilir, Quartz 4.7 ise belirtilmemişse QuartzSchedulerOptions.DefaultTimeZone'a, o da belirtilmemişse makinenin yerel saatine bakar. Container'ların çoğu UTC'de çalıştığı için yerel saate güvenmek, geliştirme makinende 02:30'da çalışan işin production'da 05:30'da çalışması demek.
Kuralı iş biriminin yaşadığı time zone'da ifade et. "Türkiye'deki müşterilere sabah 09:00'da kampanya e-postası" bir Europe/Istanbul kuralıdır; "her saat başı log arşivle" bir UTC kuralıdır. Türkiye 2016'dan beri yaz saati uygulamıyor, bu yüzden Europe/Istanbul yıl boyunca UTC+3 ve DST (Daylight Saving Time) tuzaklarından muaf:
Istanbul offset Jan=03:00:00 Jul=03:00:00
Berlin offset Jan=01:00:00 Jul=02:00:00
Ama Bookstore'un Berlin deposu için durum farklı. Ve bir gün Türkiye yeniden yaz saatine geçerse, Europe/Istanbul adını kullanan kod tzdata güncellemesiyle kendiliğinden doğru davranır; +03:00 sabitini kullanan kod ise yanlış saatte çalışmaya başlar. Time zone'u her zaman IANA (Internet Assigned Numbers Authority) adıyla ver, offset'le değil.
DST Geçişleri: Atlanan ve Tekrarlanan Saat
Baştaki soruyu Cronos ve Quartz 4.7.2 ile aynı anda sordum:
TimeZoneInfo berlin = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin");
void Show(string title, string cron, DateTimeOffset from, DateTimeOffset to)
{
Console.WriteLine($"-- {title}: '{cron}'");
var cronos = CronExpression.Parse(cron);
Console.WriteLine(" Cronos: " + string.Join(", ", cronos.GetOccurrences(from, to, berlin).Select(o => o.ToString("MM-dd HH:mm zzz"))));
var quartz = QuartzCron.Parse(cron, Quartz.CronFormat.Unix).WithTimeZone(berlin);
var list = new List<string>();
DateTimeOffset? t = quartz.GetNextValidTimeAfter(from.AddSeconds(-1));
while (t is not null && t < to) { list.Add(TimeZoneInfo.ConvertTime(t.Value, berlin).ToString("MM-dd HH:mm zzz")); t = quartz.GetNextValidTimeAfter(t.Value); }
Console.WriteLine(" Quartz: " + string.Join(", ", list));
}
var springFrom = new DateTimeOffset(2026, 3, 29, 1, 0, 0, TimeSpan.FromHours(1));
var springTo = new DateTimeOffset(2026, 3, 29, 4, 0, 0, TimeSpan.FromHours(2));
var fallFrom = new DateTimeOffset(2026, 10, 25, 1, 0, 0, TimeSpan.FromHours(2));
var fallTo = new DateTimeOffset(2026, 10, 25, 4, 0, 0, TimeSpan.FromHours(1));
Show("spring forward, daily 02:30", "30 2 * * *", springFrom, springTo.AddDays(1));
Show("fall back, daily 02:30", "30 2 * * *", fallFrom, fallTo);
Show("spring forward, every 30 min", "*/30 * * * *", springFrom, springTo);
Show("fall back, every 30 min", "*/30 * * * *", fallFrom, fallTo);
Sonuçlar:
-- spring forward, daily 02:30: '30 2 * * *'
Cronos: 03-29 03:00 +02:00, 03-30 02:30 +02:00
Quartz: 03-29 03:00 +02:00, 03-30 02:30 +02:00
-- fall back, daily 02:30: '30 2 * * *'
Cronos: 10-25 02:30 +02:00
Quartz: 10-25 02:30 +02:00
-- spring forward, every 30 min: '*/30 * * * *'
Cronos: 03-29 01:00 +01:00, 03-29 01:30 +01:00, 03-29 03:00 +02:00, 03-29 03:30 +02:00
Quartz: 03-29 01:00 +01:00, 03-29 01:30 +01:00, 03-29 03:00 +02:00, 03-29 03:30 +02:00
-- fall back, every 30 min: '*/30 * * * *'
Cronos: 10-25 01:00 +02:00, 10-25 01:30 +02:00, 10-25 02:00 +02:00, 10-25 02:30 +02:00, 10-25 02:00 +01:00, 10-25 02:30 +01:00, 10-25 03:00 +01:00, 10-25 03:30 +01:00
Quartz: 10-25 01:00 +02:00, 10-25 01:30 +02:00, 10-25 02:00 +02:00, 10-25 02:30 +02:00, 10-25 02:00 +01:00, 10-25 02:30 +01:00, 10-25 03:00 +01:00, 10-25 03:30 +01:00
İki kütüphane de aynı cevabı veriyor. İlkbaharda 02:30 diye bir an yok; günlük iş kaybolmuyor, saatin atladığı ilk geçerli ana, 03:00'e kayıyor. Sonbaharda 02:30 iki kez yaşanıyor (+02:00 ve +01:00); günlük iş yalnız ilkinde çalışıyor. Aralıklı iş ise tekrarlanan saatte iki kez çalışıyor: 02:00 ve 02:30 hem +02:00'de hem +01:00'de var. Cronos'un README'si bunu Vixie Cron davranışı olarak tarif ediyor; Quartz.NET 4.0 sürüm notu da aralıklı cron ifadelerinin tekrarlanan sonbahar saatinin iki tekrarında da tetiklendiğini, eskiden birinin atlandığını yazıyor.
Bu davranış çoğu iş için doğru, ama iki sonucu var. Günlük raporun iki çalışması arasındaki süre o gün 24 saat değil; 23 ya da 25 saat. "Son 24 saatin siparişleri" diye hesap yapan bir rapor bir saatlik veriyi atlar ya da iki kez sayar; raporu "dünün 00:00'ından bugünün 00:00'ına" gibi takvim sınırlarıyla hesapla. İkincisi, aralıklı bir iş tekrarlanan saatte iki kez çalışacağı için idempotent olmalı; yedinci yazının konusu.
Kendi Cron Scheduler'ın: Cronos ve TimeProvider
Bütün ihtiyacın "şu cron'a göre şu işi çalıştır" ise Hangfire ya da Quartz kurmadan, Cronos ve bir BackgroundService ile küçük bir scheduler yazabilirsin. Önemli bir ayrıntı: saati DateTimeOffset.UtcNow'dan değil, TimeProvider'dan oku. C# Asenkron serisinin "C# Async ve .NET 8: TimeProvider, ConfigureAwaitOptions, Parallel.ForAsync ve CancelAsync" yazısında gördüğümüz gibi, Task.Delay'in TimeProvider alan overload'u testte sahte saatle çalışmayı mümkün kılıyor.
public abstract class CronJobService(
string cron,
TimeZoneInfo timeZone,
TimeProvider time,
ILogger logger) : BackgroundService
{
private readonly CronExpression _expression = CronExpression.Parse(cron);
protected abstract Task RunAsync(CancellationToken ct);
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
DateTimeOffset now = time.GetUtcNow();
DateTimeOffset? next = _expression.GetNextOccurrence(now, timeZone);
if (next is null)
return; // ifade bir daha hiç tetiklenmeyecek
// Task.Delay'e TimeProvider veriyoruz: testte sahte saat ilerletilebilir
await Task.Delay(next.Value - now, time, stoppingToken);
try
{
await RunAsync(stoppingToken);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
logger.LogError(ex, "Scheduled run for {Occurrence} failed", next);
}
}
}
}
public sealed class NightlySalesReport(TimeProvider time, ILogger<NightlySalesReport> logger)
: CronJobService("0 3 * * *", TimeZoneInfo.FindSystemTimeZoneById("Europe/Istanbul"), time, logger)
{
public int Runs { get; private set; }
public event Action? Completed;
protected override Task RunAsync(CancellationToken ct)
{
Runs++;
logger.LogInformation("Sales report generated at {Now}", time.GetUtcNow());
Completed?.Invoke();
return Task.CompletedTask;
}
}
Döngü her turda bir sonraki çalışma anını şimdiki zamandan yeniden hesaplıyor; bu sayede uzun süren bir tur sonraki slotu kaçırırsa scheduler o slotu sonradan çalıştırmaya çalışmaz, bir sonrakini bekler. Bu, Quartz'ın misfire politikalarından birinin basit bir karşılığı; kaçırılan çalışmanın da yapılması gerekiyorsa son başarılı çalışmanın zamanını saklayıp aradaki slotları hesaplaman gerekir.
Bir de çok instance sorunu var: bu scheduler her replica'da çalışır. Sekizinci yazıdaki distributed lock'u buraya eklemek gerekecek.
FakeTimeProvider ile Test
Gece 03:00'te çalışan bir işi test etmek için gece 03:00'ü beklemek zorunda değilsin. Microsoft.Extensions.TimeProvider.Testing paketindeki FakeTimeProvider ile saati elle ilerletebilirsin:
public class NightlySalesReportTests
{
[Fact]
public async Task Runs_once_at_03_00_Istanbul_and_again_next_night()
{
// 02:00 Istanbul = 23:00 UTC
var time = new FakeTimeProvider(new DateTimeOffset(2026, 11, 8, 23, 0, 0, TimeSpan.Zero));
var report = new NightlySalesReport(time, NullLogger<NightlySalesReport>.Instance);
var ran = new SemaphoreSlim(0);
report.Completed += () => ran.Release();
await report.StartAsync(CancellationToken.None);
time.Advance(TimeSpan.FromMinutes(59)); // 02:59
Assert.False(await ran.WaitAsync(100));
Assert.Equal(0, report.Runs);
time.Advance(TimeSpan.FromMinutes(1)); // 03:00
Assert.True(await ran.WaitAsync(1000));
Assert.Equal(1, report.Runs);
time.Advance(TimeSpan.FromHours(24)); // ertesi gece 03:00
Assert.True(await ran.WaitAsync(1000));
Assert.Equal(2, report.Runs);
await report.StopAsync(CancellationToken.None);
}
}
$ dotnet test
Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1, Duration: 115 ms - Bookstore.Scheduling.Tests.dll (net10.0)
Test 115 ms'de bitti, ama içinde 25 saatlik bir zaman geçti. FakeTimeProvider.Advance, süresi dolan timer'ları tetikliyor; Task.Delay(TimeSpan, TimeProvider, CancellationToken) da o timer'lardan birini kullandığı için iş "03:00 oldu" sanıp çalışıyor. Aynı yaklaşımla DST geçiş gecesini de test edebilirsin: FakeTimeProvider'ı 28 Mart 2026 akşamına kur ve Berlin time zone'uyla saat saat ilerlet.
Üretimde Dikkat
Cron ifadesini configuration'da tutuyorsan uygulama açılırken parse et ve hatalıysa uygulamayı başlatma. Hatalı bir ifadeyi ilk çalışma anında keşfetmek, ilk gece raporun hiç çalışmaması demek. Quartz 4'te sabit ifadeler için bunu analyzer derleme zamanında yapıyor; configuration'dan gelenler için CronExpression.TryParse kullan.
Bir sonraki çalışma zamanını logla ve dashboard'da göster. "Bu iş bir sonraki sefer ne zaman çalışacak" sorusunun cevabını insanların gözüyle kontrol etmesi, time zone hatalarını en ucuz yakalama yolu. Hangfire bunu recurring job sayfasında, Quartz 4 dashboard'unda gösteriyor.
Gece yarısı ve tam saatleri bütün işlere verme. Bütün raporların 00:00'da çalışması veritabanında her gece aynı dakikada bir yük tepesi yaratır. Cronos'un H karakteri (Jenkins'ten esinlenen schedule jitter) ya da elle dağıtılmış dakikalar bu tepeyi yayar.
Sık Düşülen Tuzaklar
Quartz'a Unix ifadesi vermek. 0 0 * * * Quartz'ın varsayılan parser'ında geçersiz; CronFormat.Unix olmadan çalışmaz. Gün numaraları da farklı: Quartz'ta 1 pazar, Unix'te pazartesi.
Time zone'u belirtmemek. Yerel saate güvenen bir ifade, makinenin ya da container'ın time zone'u değişince sessizce başka saatte çalışmaya başlar.
DST'yi yok saymak. 02:00–03:00 arasına kurulu işler yılda iki gün beklenmedik davranır. Mümkünse bu aralığı kullanma, kullanıyorsan işi idempotent yap ve hesaplarını takvim sınırlarıyla kur.
"Her 24 saatte bir" ile "her gün 03:00'te"yi karıştırmak. İlki bir aralık, ikincisi bir duvar saati kuralı; DST günlerinde ve uygulama yeniden başladığında farklı sonuç verirler.
Küçük Bir Deneme
★☆☆ Tablodaki "ayın son cuması 18:00" ifadesinin 2027 yılı boyunca bütün çalışma zamanlarını Cronos'un GetOccurrences method'uyla listele. Kaç tanesi ayın 25'inden önce?
★★☆ NightlySalesReport'u Europe/Berlin ve 30 2 * * * ile kur, FakeTimeProvider'ı 28 Mart 2026 22:00 UTC'ye ayarla ve saat saat ilerleterek raporun kaçta çalıştığını test et.
★★★ CronJobService'e kaçırılan çalışmaları telafi etme özelliği ekle: son başarılı çalışma zamanını bir tabloda sakla ve açılışta arada kalan slot sayısına göre en fazla bir kez çalıştır. Bu davranış Quartz'ın hangi misfire instruction'ına benziyor?
Bir sonraki yazıda zamanlamayı ve kalıcılığı birlikte çözen ilk büyük araca geçiyoruz: Hangfire ile fire-and-forget, delayed, recurring ve continuation job'lar, job'ların SQL (Structured Query Language) Server'da nasıl saklandığı ve method imzasını değiştirdiğinde eski job'lara ne olduğu.