Debouncer.Sharp
1.1.6
See the version list below for details.
dotnet add package Debouncer.Sharp --version 1.1.6
NuGet\Install-Package Debouncer.Sharp -Version 1.1.6
<PackageReference Include="Debouncer.Sharp" Version="1.1.6" />
<PackageVersion Include="Debouncer.Sharp" Version="1.1.6" />
<PackageReference Include="Debouncer.Sharp" />
paket add Debouncer.Sharp --version 1.1.6
#r "nuget: Debouncer.Sharp, 1.1.6"
#:package Debouncer.Sharp@1.1.6
#addin nuget:?package=Debouncer.Sharp&version=1.1.6
#tool nuget:?package=Debouncer.Sharp&version=1.1.6
Debouncer.Sharp
Thread-safe debounce primitives for delayed, coalesced async work.
Three tiers, pick the one that matches your use case:
| Tier | Answers | Example |
|---|---|---|
Action (IDebouncer) |
"Run this after a quiet period, cancel-and-replace on every hit." | Search-as-you-type, UI refresh, validation |
Latest (IDebouncedLatest<T>) |
"Keep only the newest value, apply it once after a quiet period." | Autosave, settings persistence, snapshot writes |
Batch (IDebouncedBatch<T>) |
"Collect everything, flush the batch after a quiet period — but never delay forever under continuous load." | Telemetry, logs, audit trails, burst writes |
Install
<PackageReference Include="Debouncer.Sharp" Version="1.0.*" />
Or via CLI:
dotnet add package Debouncer.Sharp
Versioning follows Semantic Versioning. Releases are cut by pushing a vX.Y.Z git tag; see CHANGELOG.md for release history.
Releasing
The Publish NuGet workflow only runs on vX.Y.Z tag pushes, and will fail unless the .csproj version and CHANGELOG.md are both updated first. To cut a release:
- Bump
<Version>inDebouncer.Sharp.csproj. - Move the relevant
[Unreleased]entries inCHANGELOG.mdinto a new## [X.Y.Z] - YYYY-MM-DDsection. - Commit both changes.
- Tag and push:
git tag vX.Y.Z && git push origin vX.Y.Z— this triggers the publish workflow.
Basic debouncer
using Debouncer.Sharp;
using var debouncer = new Debouncer(TimeSpan.FromMilliseconds(250));
await debouncer.HitAsync(async ct =>
{
await PersistAsync(ct);
});
Each hit cancels any pending action. Only the latest action runs after the delay window.
Async fetch + apply
using Debouncer.Sharp;
var operation = new DebouncedAsync<string, SearchResult>(
TimeSpan.FromMilliseconds(300),
fetchAsync: SearchAsync,
apply: (_, result) => ViewModel.Apply(result),
shouldApply: (query, _) => query == ViewModel.CurrentQuery,
cache: null,
cacheTtl: TimeSpan.FromSeconds(30),
dispatcher: null);
If you need UI-thread marshalling, pass a GuiDispatcher.Sharp IGuiDispatcher to
DebouncedAsync or DebounceFactory — e.g. AvaloniaGuiDispatcher from
GuiDispatcher.Sharp.Avalonia, or ImmediateGuiDispatcher for headless/test hosts.
Latest value (coalesce to newest)
using Debouncer.Sharp;
using var settingsSave = factory.CreateLatest<Settings>(
TimeSpan.FromMilliseconds(750),
applyAsync: async (settings, ct) => await SaveToDiskAsync(settings, ct));
settingsSave.Hit(currentSettings); // rapid calls coalesce to the last value
Important:
Dispose()only cancels the pending timer — it does not flush. If you need a guaranteed write before shutdown,await FlushAsync()explicitly before disposing:await settingsSave.FlushAsync(); settingsSave.Dispose();This avoids a sync-over-async deadlock risk when
IGuiDispatchermarshals onto a UI thread that a blocking flush-on-dispose could otherwise block against.
Batch (collect then flush, with caps)
using Debouncer.Sharp;
using var telemetry = factory.CreateBatch<TelemetryEvent>(
TimeSpan.FromSeconds(1),
flushAsync: async (events, ct) => await SendBatchAsync(events, ct),
options: new DebouncedBatchOptions
{
MaxBatchSize = 1000, // flush as soon as the buffer reaches this many items
MaxWait = TimeSpan.FromSeconds(5), // flush even if items keep arriving without a quiet period
});
telemetry.Add(evt);
A pure debounce would postpone the flush forever under continuous load, since the quiet period
never elapses. MaxBatchSize and MaxWait are hard caps that force a flush regardless of ongoing
activity — MaxWait should typically be >= delay. Same Dispose()-does-not-flush caveat as
IDebouncedLatest<T> applies here.
File system sinks (Debouncer.Sharp.FileSystem)
Ready-made sinks for the most common debounce use case — coalescing writes to a file:
using Debouncer.Sharp.FileSystem;
using System.Text.Json.Serialization;
[JsonSerializable(typeof(Settings))]
internal partial class AppJsonContext : JsonSerializerContext { }
// Atomic JSON snapshot: writes to a temp sibling, then moves it into place.
using var settingsSave = factory.CreateJsonSnapshot(
"settings.json",
TimeSpan.FromMilliseconds(750),
AppJsonContext.Default.Settings,
onError: ex => log.Warning(ex, "Failed to save settings"));
settingsSave.Hit(currentSettings);
// JSON Lines batch: plain .jsonl append, or additive bursts inside a .jsonl.zip archive.
using var telemetry = factory.CreateJsonLinesBatch(
"telemetry.jsonl",
TimeSpan.FromSeconds(1),
AppJsonContext.Default.TelemetryEvent,
options: new DebouncedBatchOptions { MaxBatchSize = 1000 },
zip: true, // omit or set false for a plain, human-readable .jsonl file
onError: ex => log.Warning(ex, "Failed to write telemetry batch"));
telemetry.Add(evt);
// Reading back a zip-mode batch:
var events = JsonlZipReader.ReadAllRecords("telemetry.jsonl.zip", AppJsonContext.Default.TelemetryEvent);
Both sinks take an optional onError callback invoked instead of throwing when the write fails
(a transient disk error shouldn't crash the debouncer); without it, failures propagate through
FlushAsync()/the discarded background task, same as the raw tiers. The zip sink also accepts an
optional onDiagnostic callback for informational messages (e.g. burst-sequence recovery on
restart). There's no logging-framework dependency — these are plain Action delegates, by design.
Deterministic testing
Debounce timers use the standard .NET TimeProvider abstraction. Pass a FakeTimeProvider
(Microsoft.Extensions.TimeProvider.Testing) to make quiet-period behaviour deterministic in tests:
using Microsoft.Extensions.Time.Testing;
var time = new FakeTimeProvider();
using var latest = new DebouncedLatest<int>(
TimeSpan.FromMilliseconds(250),
applyAsync: async (value, ct) => { /* ... */ },
timeProvider: time);
latest.Hit(1);
latest.Hit(2);
time.Advance(TimeSpan.FromMilliseconds(250));
await Task.Yield();
// apply ran for value 2 — no real-time sleep required
The same optional timeProvider is available on Debouncer, DebouncedAsync<TIn,TOut>,
DebouncedBatch<T>, and DebounceFactory. Omit it in production code.
Dependency injection
using Debouncer.Sharp;
using GuiDispatcher.Sharp;
using Microsoft.Extensions.DependencyInjection;
services.AddDebouncing();
// Optional: register a dispatcher for UI-thread apply
services.AddSingleton<IGuiDispatcher, MyUiDispatcher>();
var factory = serviceProvider.GetRequiredService<IDebounceFactory>();
var cache = serviceProvider.CreateDebounceCache<string, SearchResult>("search");
// The same factory also exposes the Latest and Batch tiers:
var settingsSave = factory.CreateLatest<Settings>(TimeSpan.FromMilliseconds(750), SaveAsync);
var telemetry = factory.CreateBatch<TelemetryEvent>(TimeSpan.FromSeconds(1), SendBatchAsync);
API overview
| Type | Purpose |
|---|---|
Debouncer |
Simple delayed action debouncing |
DebouncedAsync<TIn, TOut> |
Debounced fetch with optional cache and apply |
DebouncedLatest<T> |
Coalesces to the newest value, applies once after a quiet period |
DebouncedBatch<T> |
Buffers items, flushes as a batch with MaxBatchSize/MaxWait caps |
DebouncedBatchOptions |
MaxBatchSize/MaxWait caps for IDebouncedBatch<T> |
DebounceFactory |
Creates debouncers with optional dispatcher |
IDebounceCache<TKey, TValue> |
Cache abstraction |
MemoryCacheDebounceCache<TKey, TValue> |
IMemoryCache-backed cache |
SimpleDictionaryCache<TKey, TValue> |
In-memory dictionary cache |
DebounceServiceExtensions |
DI registration helpers |
DebounceFileSystemExtensions (Debouncer.Sharp.FileSystem) |
CreateJsonSnapshot/CreateJsonLinesBatch file sinks |
JsonlZipReader (Debouncer.Sharp.FileSystem) |
Reads back a zip-mode JSON Lines batch |
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- GuiDispatcher.Sharp (>= 1.0.2 && < 2.0.0)
- Microsoft.Extensions.Caching.Memory (>= 10.0.9)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.9)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Debouncer.Sharp:
| Package | Downloads |
|---|---|
|
ComposableSettings
Component-based settings composition with source-generated child wiring, registration, and DI. |
GitHub repositories
This package is not used by any popular GitHub repositories.