Skip to content

.NET

The Whisperr server-side SDK for .NET — for ASP.NET Core, workers, serverless handlers, and any backend that knows the end-user id.

Terminal window
dotnet add package Whisperr
using Whisperr;
var whisperr = new WhisperrClient(Environment.GetEnvironmentVariable("WHISPERR_API_KEY")!);
whisperr.Track(
"user_8842",
"payment_failed",
new Dictionary<string, object?>
{
["amount_cents"] = 4900,
["reason"] = "card_declined"
});
whisperr.Identify("user_8842", new IdentifyOptions
{
Email = "ada@example.com",
Traits = new Dictionary<string, object?>
{
["plan"] = "pro"
}
});
await whisperr.ShutdownAsync();

The user id is always explicit. Pass the same external_user_id your app uses everywhere else and backend events land on the same timeline as frontend events.

Install the companion package for dependency-injection wiring:

Terminal window
dotnet add package Whisperr.AspNetCore

AddWhisperr binds the Whisperr configuration section (falling back to the WHISPERR_API_KEY environment variable) and registers WhisperrClient as a singleton. The host disposes the singleton on shutdown, which runs a final flush — you don’t manage the lifetime yourself.

using Whisperr.AspNetCore;
builder.Services.AddWhisperr(builder.Configuration, options =>
{
options.OnError = error => Console.Error.WriteLine($"whisperr:{error.Code} {error.Message}");
});
appsettings.json
{
"Whisperr": {
"ApiKey": "wrk_...",
"FlushInterval": "00:00:10"
}
}

Inject WhisperrClient wherever you have the user id. Track/Identify enqueue and return immediately — let the background flush deliver them rather than awaiting FlushAsync on the request path (a flush can wait through retry backoff):

app.MapPost("/billing/webhook", (
BillingWebhook webhook,
WhisperrClient whisperr) =>
{
whisperr.Track(webhook.UserId, "payment_failed", new Dictionary<string, object?>
{
["amount_cents"] = webhook.AmountCents,
["provider"] = "stripe"
});
return Results.Ok();
});

For handlers that already authenticate a user, add the middleware after authentication and use HttpContext.Whisperr(), which binds to the user id from the NameIdentifier/sub claim (override via UseWhisperr(resolveUser)):

using Whisperr.AspNetCore;
app.UseAuthentication();
app.UseWhisperr();
app.MapPost("/plan/upgrade", (HttpContext http) =>
{
http.Whisperr().Track("plan_upgraded", new Dictionary<string, object?> { ["plan"] = "pro" });
return Results.Ok();
});

Implements the full delivery contract: batching to /v1/events/batch, snake_case validation before enqueue, a stable $message_id per event across retries, retain-on-auth, bounded backoff on transient failures, drop on malformed 4xx.

The queue is in-process, not crash-durable. Call FlushAsync or ShutdownAsync before short-lived processes exit.

  • Targets: net8.0 and netstandard2.0 (covers .NET Framework 4.7.2+). The conformance suite runs on both net8.0 and net472 in CI.
  • Long-lived hosts: hold one client for the process lifetime; in ASP.NET Core the DI singleton is flushed and disposed on shutdown.
  • Short-lived / serverless: await client.ShutdownAsync() (or FlushAsync()) before returning so buffered events aren’t lost.
  • Trimming / AOT: trimmed deployments are supported. The SDK uses reflection-based System.Text.Json, so Native AOT is not a v1 guarantee; prefer JIT/ReadyToRun runtimes for now.
Option Default Notes
ApiKey App ingestion key (wrk_...). Required unless disabled.
BaseUrl https://api.whisperr.net Whisperr ingestion API.
FlushAt 20 Flush when this many operations are queued.
FlushInterval 10s Background flush cadence. TimeSpan.Zero disables.
MaxQueueSize 10000 Oldest operations drop on overflow.
MaxBatchSize 500 Events per batch; backend cap is 500.
MaxRetries 6 Transient retries before retaining for a later flush.
RequestTimeout 10s Per-request timeout.
Disabled false No-op client for tests and local modes.
Debug false Diagnostic warnings to Logger or stderr.
OnError Observability hook for auth, dropped, retry_exhausted.
HttpClient private client Pass your own for custom handlers or test capture.