Dloizides.Webhooks.AspNetCore
1.1.0
dotnet add package Dloizides.Webhooks.AspNetCore --version 1.1.0
NuGet\Install-Package Dloizides.Webhooks.AspNetCore -Version 1.1.0
<PackageReference Include="Dloizides.Webhooks.AspNetCore" Version="1.1.0" />
<PackageVersion Include="Dloizides.Webhooks.AspNetCore" Version="1.1.0" />
<PackageReference Include="Dloizides.Webhooks.AspNetCore" />
paket add Dloizides.Webhooks.AspNetCore --version 1.1.0
#r "nuget: Dloizides.Webhooks.AspNetCore, 1.1.0"
#:package Dloizides.Webhooks.AspNetCore@1.1.0
#addin nuget:?package=Dloizides.Webhooks.AspNetCore&version=1.1.0
#tool nuget:?package=Dloizides.Webhooks.AspNetCore&version=1.1.0
Webhooks.AspNetCore
Durable outbound-webhook delivery for multi-tenant ASP.NET Core SaaS — a per-endpoint transactional outbox.
Each (event × subscribed endpoint) is its own outbox row, delivered and retried
independently on its own back-off. A failing endpoint reschedules only its own row and
never re-delivers to a healthy sibling — the reason to reach for this instead of a
per-event fan-out. HMAC-signed POSTs with configurable headers, a scale-out-safe xmin-lease
processor, dead-lettering, and per-attempt delivery history.
The consuming product brings its DbContext (which includes the outbox tables) and an
endpoint resolver; everything else is wired in ~5 lines of Program.cs.
Concepts
WebhookOutboxMessage— one row per(event, endpoint). All the endpoint rows of one logical event share a single stableEventId(the idempotency key the receiver dedups on), but each carries its ownEndpointIdand retries alone.IWebhookEndpointResolver— the consumer's only required seam: given(tenantId, eventType)it returns the subscribed endpoints (id, URL, signing-secret ciphertext, active). Called at enqueue (to fan out) and at send (to pick up a rotated secret / changed URL, and to drop a since-deleted endpoint cleanly).ISecretProtector(fromSecurity.SecretProtector) — the send-time "secret decryptor". Secrets are never written into the outbox table; the package decrypts the endpoint's ciphertext only when it signs.IWebhookSigner— the signature scheme. Defaults to HMAC-SHA256 over{timestamp}.{body}renderedsha256=<hex>. Register your own beforeAddWebhookDeliveryto override it.WebhookHeaderOptions— the header names (event-type, idempotency id, signature, timestamp). Default toX-Webhook-*; override to keep your existing wire contract.- The processor + hosted service — claims due rows with an xmin-conditional lease (safe
with more than one replica; a claim held by a dead worker lapses and is reclaimed), delivers
off-request, and reschedules on the configured back-off (default
5m → 1h → 5h → 18h, then dead-letter).
Wiring (consumer side)
MyDbContext:
public sealed class MyDbContext : DbContext, IWebhookOutboxDbContext
{
public DbSet<WebhookOutboxMessage> WebhookOutbox => Set<WebhookOutboxMessage>();
public DbSet<WebhookDeliveryRecord> WebhookDeliveries => Set<WebhookDeliveryRecord>();
protected override void OnModelCreating(ModelBuilder b)
{
b.AddWebhookOutbox(useXminConcurrency: Database.IsNpgsql());
}
}
Program.cs:
services.AddScoped<IWebhookOutboxDbContext>(sp => sp.GetRequiredService<MyDbContext>());
services.AddScoped<IWebhookEndpointResolver, MyEndpointResolver>();
services.AddSingleton<ISecretProtector>(/* your configured protector */);
services.AddWebhookDelivery(configuration, headers =>
{
headers.EventTypeHeader = "X-My-Event";
headers.EventIdHeader = "X-My-Event-Id";
});
Emit an event from anywhere (best-effort — enqueuing never fails the caller):
await dispatcher.DispatchAsync(tenantId, "screening.completed", payload, ct); // IWebhookOutboxDispatcher
appsettings.json:
{
"Webhooks": {
"Outbox": {
"Enabled": true,
"PollIntervalSeconds": 15,
"LeaseTtlSeconds": 120,
"BatchSize": 20,
"DeliveryTimeoutSeconds": 10,
"DeliveryRetentionDays": 7,
"BackoffSeconds": [300, 3600, 18000, 64800]
}
}
}
BackoffSeconds is one entry per retry (the first attempt is immediate). The attempt budget is
its length + 1; an empty list means "no retries — dead-letter on the first failure".
What the consumer owns
- The webhook-endpoint storage (URL + encrypted signing secret + subscribed event types), read
through
IWebhookEndpointResolver. - The
DbContextand its migrations — callAddWebhookOutbox()inOnModelCreating, thendotnet ef migrations add AddWebhookOutbox. Because the outbox rows are written through the consumer's context, enqueue commits in the same transaction as the work that produced the event (the transactional-outbox guarantee). - The
ISecretProtectorkey sourcing (the package owns the crypto viaSecurity.SecretProtector; the app owns the key). - Serving the delivery history to tenants if desired (read
WebhookDeliveries).
Security
- Signing secrets are decrypted only at send time and never stored in the outbox table.
- The signature binds a per-attempt timestamp (
{timestamp}.{body}) so receivers can bound replay. - Delivery failures are isolated per endpoint and never thrown, so emitting a webhook can't fail the operation that produced it.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. 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. |
-
net8.0
- Microsoft.EntityFrameworkCore (>= 8.0.11)
- Microsoft.EntityFrameworkCore.Relational (>= 8.0.11)
- Microsoft.Extensions.Http.Resilience (>= 8.10.0)
- Security.SecretProtector (>= 1.0.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.