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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Dloizides.Webhooks.AspNetCore" Version="1.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Dloizides.Webhooks.AspNetCore" Version="1.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Dloizides.Webhooks.AspNetCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Dloizides.Webhooks.AspNetCore --version 1.1.0
                    
#r "nuget: Dloizides.Webhooks.AspNetCore, 1.1.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Dloizides.Webhooks.AspNetCore@1.1.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Dloizides.Webhooks.AspNetCore&version=1.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Dloizides.Webhooks.AspNetCore&version=1.1.0
                    
Install as a Cake Tool

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 stable EventId (the idempotency key the receiver dedups on), but each carries its own EndpointId and 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 (from Security.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} rendered sha256=<hex>. Register your own before AddWebhookDelivery to override it.
  • WebhookHeaderOptions — the header names (event-type, idempotency id, signature, timestamp). Default to X-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 DbContext and its migrations — call AddWebhookOutbox() in OnModelCreating, then dotnet 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 ISecretProtector key sourcing (the package owns the crypto via Security.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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.1.0 79 7/21/2026
1.0.1 195 7/17/2026