Kanject.Core.Recurring

View .md

Attribute-driven recurring method invocation with a drift-corrected engine and an AOT-safe source generator. Annotate a method with a rate, and a {Method}RecurringAsync extension is generated — call it from a console app, a Lambda handler, or an IHostedService. Cadence and lifecycle live on the declaration; the call site is just await .RecurringAsync(...).

Install

bash
dotnet add package Kanject.Core

The attribute and engine ship in Kanject.Core — no separate package, and it works immediately after adding the reference.

Annotate a method

csharp
using Kanject.Core.Annotations.Attributes.Recurring;public sealed class HeartbeatService(IPingClient client){    // Cadence lives on the method. Units: Seconds | Minutes | Hours | Days.    [Recurring(5, RecurringRateUnit.Seconds)]    public async Task SendHeartbeatAsync(CancellationToken cancellationToken)        => await client.PingAsync(cancellationToken);}

The rate is a value plus a RecurringRateUnit (Seconds, Minutes, Hours, Days). The engine uses Stopwatch.GetTimestamp for drift-corrected scheduling — if one iteration runs long, the next still fires on the original cadence rather than accumulating lag.

Call it

csharp
// The generator emits {Method}RecurringAsync — call it anywhere: a console// app, a Lambda handler, or an IHostedService. No timer, no host required.var service = new HeartbeatService(client);RecurringResult result = await service.SendHeartbeatRecurringAsync(    cancellationToken: ct);Console.WriteLine($"Ran {result.Iterations} times, stopped because {result.StopReason}");

The generated extension runs the loop and returns a RecurringResult (Iterations, StopReason, LastException). Value-returning methods get three overloads — including a …RecurringToLastAsync that returns the final value and a streaming variant. There is no reflection and no DI requirement; the generator wires everything at compile time.

Bound and tune a run

csharp
// Bound a run by count or wall-clock, and add jitter to de-sync fleets.[Recurring(1, RecurringRateUnit.Hours,    MaxIterations = 24,        // stop after 24 runs    MaxDurationSeconds = 3600, // …or after an hour, whichever first    MaxJitterMs = 500,         // spread ticks across instances    StopOnException = true)]   // surface faults instead of swallowing thempublic Task ReconcileAsync(CancellationToken ct) => _ledger.ReconcileAsync(ct);

What ships with it

  • [Recurring(rateValue, RecurringRateUnit.Seconds|Minutes|Hours|Days)] on a method — cadence as declaration, not embedded config
  • Generated {Method}RecurringAsync (+ value-returning and streaming overloads) — runs in Lambda, console, or IHostedService unchanged
  • Drift-corrected scheduling via Stopwatch.GetTimestamp — no accumulated lag
  • MaxIterations, MaxDurationSeconds, MaxJitterMs, StopOnException bounds; overrun and exception policies with per-iteration and global hooks
  • RecurringResult (Iterations / StopReason / LastException) instead of silently-swallowed loop errors
Was this page helpful?