@@ -12,6 +12,7 @@ A lightweight, high-performance, thread-safe background work queue manager and d
1212- 🔒 ** Safe Background Execution** : No scoped service leaks across async boundaries
1313- 🔄 ** Context Capture & Rehydration** : Automatically captures and restores context (user claims, tenant ID, etc.)
1414- ⚙️ ** Flexible Handler Pipeline** : Multiple handlers can process the same work type with ordered execution
15+ - ⏱️ ** Rate Control** : Built-in debounce and throttle scheduling for controlling work execution timing
1516- 📊 ** Observable** : Structured logging at all execution boundaries
1617- 🎯 ** Multi-Targeting** : Supports .NET 6.0 through .NET 10.0
1718- 📚 ** Fully Documented** : Comprehensive XML documentation for all public APIs
@@ -24,6 +25,7 @@ XpressWork provides safe, in-memory background work execution for .NET applicati
2425- Request context (user identity, tenant, etc.) is captured and restored
2526- Multiple handlers can process work items in deterministic order
2627- Proper cancellation and exception handling
28+ - Rate-controlled scheduling with debounce and throttle patterns
2729
2830## What XpressWork Is NOT
2931
@@ -174,23 +176,130 @@ await submitter.Enqueue(new BackgroundActionWorkArguments(
174176 Name : " ProcessOrder" ));
175177```
176178
179+ ## Rate-Controlled Scheduling
180+
181+ XpressWork includes a scheduling layer for rate-controlling when work is submitted to the queue. This is useful for scenarios where you need to coalesce rapid events or limit execution frequency.
182+
183+ ### Enable Scheduling
184+
185+ ``` csharp
186+ services .AddXpressWork <MyAppContext , MyAppScopeFactory >();
187+ services .AddXpressWorkScheduling <MyAppContext >();
188+ ```
189+
190+ ### Debounce Pattern
191+
192+ Execute work only after a period of inactivity. Useful for auto-save scenarios where you want to save only after the user stops typing.
193+
194+ ``` csharp
195+ public class PreferencesService
196+ {
197+ private readonly IBackgroundWorkScheduler <MyAppContext > _scheduler ;
198+
199+ public PreferencesService (IBackgroundWorkScheduler <MyAppContext > scheduler )
200+ {
201+ _scheduler = scheduler ;
202+ }
203+
204+ public async Task SavePreferencesAsync (Guid userId , UserPreferences prefs )
205+ {
206+ // Only save after 500ms of inactivity
207+ // Multiple rapid calls reset the timer
208+ await _scheduler .Debounce (
209+ interval : TimeSpan .FromMilliseconds (500 ),
210+ key : userId , // Unique key per user
211+ workArgument : new SavePreferencesArgs { UserId = userId , Preferences = prefs });
212+ }
213+ }
214+ ```
215+
216+ ### Throttle Pattern
217+
218+ Execute work at most once per interval. Useful for sensor data or metrics where you want regular updates regardless of input frequency.
219+
220+ ``` csharp
221+ public class SensorService
222+ {
223+ private readonly IBackgroundWorkScheduler <MyAppContext > _scheduler ;
224+
225+ public async Task ProcessTemperatureAsync (Guid sensorId , double temperature )
226+ {
227+ // Process at most once per second, using the latest reading
228+ await _scheduler .Throttle (
229+ interval : TimeSpan .FromSeconds (1 ),
230+ key : sensorId ,
231+ workArgument : new ProcessTemperatureArgs { SensorId = sensorId , Temperature = temperature });
232+ }
233+ }
234+ ```
235+
236+ ### Run-Once-Then Patterns
237+
238+ Execute immediately on first call, then apply rate limiting for subsequent calls.
239+
240+ ``` csharp
241+ // Execute first call immediately, then debounce subsequent calls
242+ await _scheduler .RunOnceThenDebounce (
243+ interval : TimeSpan .FromMilliseconds (500 ),
244+ key : userId ,
245+ workArgument : new SaveArgs { .. . });
246+
247+ // Execute first call immediately, then throttle subsequent calls
248+ await _scheduler .RunOnceThenThrottle (
249+ interval : TimeSpan .FromSeconds (1 ),
250+ key : sensorId ,
251+ workArgument : new ProcessArgs { .. . });
252+ ```
253+
254+ ### Cancellation and Flushing
255+
256+ ``` csharp
257+ // Cancel pending scheduled work
258+ var (action , parameters ) = _scheduler .Cancel <SavePreferencesArgs >(userId );
259+
260+ // Flush all pending work immediately (also called on shutdown)
261+ await _scheduler .FlushAsync ();
262+
263+ // Discard all pending work without execution
264+ _scheduler .Clear ();
265+ ```
266+
177267## Architecture Overview
178268
179269| Component | Lifetime | Responsibility |
180270| -----------| ----------| ----------------|
181271| ` BackgroundWorkQueue<TContext> ` | ** Singleton** | Owns channel, scheduling, execution |
182272| ` IBackgroundWorkSubmitter<TContext> ` | ** Scoped** | Captures caller scope & enqueues |
273+ | ` IBackgroundWorkScheduler<TContext> ` | ** Singleton** | Rate-controlled scheduling (debounce/throttle) |
183274| ` IBackgroundWorkScopeFactory<TContext> ` | Singleton | Capture + rehydrate logic |
184275| ` IBackgroundWorkContextAccessor<TContext> ` | Scoped | Holds rehydrated context |
185276| ` IWorkHandler<TArgs> ` | Scoped | Executes work |
186277
278+ ### Conceptual Architecture
279+
280+ ```
281+ ┌──────────────────────────────────────────────────────────────┐
282+ │ Your Application │
283+ ├──────────────────────────────────────────────────────────────┤
284+ │ IBackgroundWorkScheduler │ IBackgroundWorkSubmitter │
285+ │ (rate-controlled scheduling) │ (immediate submission) │
286+ ├──────────────────────────────────────────────────────────────┤
287+ │ IBackgroundWorkQueue │
288+ │ (execution pipeline) │
289+ ├──────────────────────────────────────────────────────────────┤
290+ │ IWorkHandler<T> │ IWorkHandler<T> │ IWorkHandler<T> │
291+ │ (ordered execution in isolated scopes) │
292+ └──────────────────────────────────────────────────────────────┘
293+ ```
294+
187295### Key Invariants
188296
1892971 . ** Background execution never uses the original request/caller scope**
1902982 . ** No scoped services are captured and reused across async boundaries**
1912993 . ** Each work item executes in a new ` AsyncServiceScope ` **
1923004 . ** Context is rehydrated before resolving handlers**
1933015 . ** Handler ordering is deterministic and stable**
302+ 6 . ** Scheduler flushes all pending work on graceful shutdown**
194303
195304## Configuration Options
196305
@@ -233,20 +342,24 @@ Handlers with the same `Order` value are sorted deterministically by type name.
233342- Resolve services inside handlers, not in work arguments
234343- Use the scoped submitter from within a DI scope
235344- Handle cancellation tokens properly in handlers
345+ - Use debounce for user input that triggers saves/updates
346+ - Use throttle for high-frequency events (sensors, metrics)
236347
237348### 🚫 DON'T
238349
239350- Capture scoped services in work arguments
240351- Store ` IServiceProvider ` or ` HttpContext ` in work arguments
241352- Resolve the submitter from a singleton
242353- Use XpressWork for work that requires persistence or distribution
354+ - Use very short debounce/throttle intervals (< 10ms)
243355
244356## Thread Safety
245357
246358All operations are thread-safe:
247359- Multiple producers can enqueue concurrently
248360- Context does not bleed across work items
249361- Queue processing is sequential by default
362+ - Scheduler operations are thread-safe with concurrent dictionary storage
250363
251364## Examples
252365
0 commit comments