LowCodeHub.Webhooks 0.0.1

There is a newer version of this package available.
See the version list below for details.
dotnet add package LowCodeHub.Webhooks --version 0.0.1
                    
NuGet\Install-Package LowCodeHub.Webhooks -Version 0.0.1
                    
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="LowCodeHub.Webhooks" Version="0.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="LowCodeHub.Webhooks" Version="0.0.1" />
                    
Directory.Packages.props
<PackageReference Include="LowCodeHub.Webhooks" />
                    
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 LowCodeHub.Webhooks --version 0.0.1
                    
#r "nuget: LowCodeHub.Webhooks, 0.0.1"
                    
#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 LowCodeHub.Webhooks@0.0.1
                    
#: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=LowCodeHub.Webhooks&version=0.0.1
                    
Install as a Cake Addin
#tool nuget:?package=LowCodeHub.Webhooks&version=0.0.1
                    
Install as a Cake Tool

LowCodeHub.Webhooks

A dynamic webhook invocation library for ASP.NET Core with database-driven subscriptions, outbox-based reliable delivery, resilience policies (via Microsoft.Extensions.Http.Resilience / Polly v8), and optional multi-tenant support.

Features

  • Database-driven subscriptions — Webhook URLs, HTTP methods, headers, timeouts, and event types are stored in a configurable database table
  • Outbox-based delivery — Delivery attempts are persisted before sending, ensuring at-least-once delivery even across crashes and restarts
  • Two-level resilience — Immediate transient-fault retries (Polly) on each HTTP call + durable exponential-backoff retries via the outbox worker
  • Multi-tenant support — Optional TenantId on subscriptions and deliveries for multi-tenant/multi-client scenarios
  • Typed events — Each subscription is tied to a specific event type (e.g. "order.created", "payment.completed")
  • Configurable HTTP method — POST, PUT, PATCH, or any HTTP method per subscription
  • Custom headers — JSON-serialized headers per subscription (e.g. API keys, auth tokens)
  • Snapshot-at-dispatch — Subscription config (URL, headers, method) is snapshotted into the delivery attempt at dispatch time, so later subscription changes don't affect in-flight deliveries
  • Native SQL Server & PostgreSQL — Provider-specific implementations with pessimistic locking for safe multi-instance processing
  • Health checks — Liveness (worker heartbeat) and readiness (DB connectivity) health checks
  • Graceful shutdown — Drain in-flight deliveries before stopping

Installation

// Add core webhook services + delivery worker
builder.Services
    .AddWebhooks(builder.Configuration)
    .AddWebhooksSqlServer(builder.Configuration);     // or AddWebhooksPostgreSql

Configuration

appsettings.json

{
  "Webhooks": {
    "Enabled": true,
    "BatchSize": 50,
    "MaxParallelism": 4,
    "PollInterval": "00:00:02",
    "InitialRetryDelay": "00:00:05",
    "MaxRetryDelay": "00:05:00",
    "DefaultMaxRetries": 5,
    "DefaultTimeoutSeconds": 30,
    "DrainOnShutdown": true,
    "DrainTimeout": "00:00:30",
    "HealthStaleAfter": "00:02:00",
    "HttpRetryCount": 3,
    "HttpRetryBaseDelay": "00:00:01",
    "HttpTotalTimeout": "00:01:00",
    "SqlServer": {
      "ConnectionString": "Server=...;Database=...;",
      "SubscriptionSchema": "dbo",
      "SubscriptionTable": "WebhookSubscriptions",
      "DeliverySchema": "dbo",
      "DeliveryTable": "WebhookDeliveryAttempts",
      "LeaseDuration": "00:02:00"
    }
  }
}

PostgreSQL variant

{
  "Webhooks": {
    "PostgreSql": {
      "ConnectionString": "Host=...;Database=...;",
      "SubscriptionSchema": "public",
      "SubscriptionTable": "webhook_subscriptions",
      "DeliverySchema": "public",
      "DeliveryTable": "webhook_delivery_attempts",
      "LeaseDuration": "00:02:00"
    }
  }
}

Usage

Dispatching Webhooks

public class OrderService(IWebhookDispatcher webhookDispatcher)
{
    public async Task CreateOrderAsync(Order order, CancellationToken ct)
    {
        // ... create order logic ...

        // Dispatch webhook to all active subscriptions for "order.created"
        await webhookDispatcher.DispatchAsync(
            eventType: "order.created",
            payload: new { order.Id, order.Total, order.CreatedAt },
            tenantId: order.TenantId,   // optional
            cancellationToken: ct);
    }
}

Without Multi-Tenancy

await webhookDispatcher.DispatchAsync("user.registered", new { userId, email });

How It Works

1. DispatchAsync("order.created", payload, tenantId)
   → Queries active subscriptions matching event type + tenant
   → Snapshots each subscription's config into a WebhookDeliveryAttempt (Pending)
   → Persists to DeliveryAttempts table

2. WebhookDeliveryWorker (background service, polls every 2s):
   → Claims batch of pending deliveries (atomic lock: UPDLOCK/READPAST or FOR UPDATE SKIP LOCKED)
   → Sends HTTP request via resilience-enhanced HttpClient (immediate retries for transient failures)
   → Success (2xx) → MarkDelivered
   → Failure → MarkFailed with exponential backoff next attempt
   → Max retries exhausted → permanently Failed

Two-Level Resilience

Level Mechanism Scope Typical Duration
Immediate Microsoft.Extensions.Http.Resilience (Polly v8) Per HTTP call Seconds (retry transient 5xx, network blips)
Durable Outbox worker + exponential backoff Across process restarts Minutes to hours (extended outages)

SQL Server Migration

-- Subscriptions table
CREATE TABLE [dbo].[WebhookSubscriptions]
(
    [Id]              UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT NEWID(),
    [TenantId]        NVARCHAR(200)    NULL,
    [EventType]       NVARCHAR(200)    NOT NULL,
    [Name]            NVARCHAR(200)    NOT NULL,
    [WebhookUrl]      NVARCHAR(2000)   NOT NULL,
    [HttpMethod]      NVARCHAR(10)     NOT NULL DEFAULT 'POST',
    [ContentType]     NVARCHAR(100)    NOT NULL DEFAULT 'application/json',
    [Headers]         NVARCHAR(MAX)    NULL,
    [TimeoutSeconds]  INT              NOT NULL DEFAULT 30,
    [MaxRetries]      INT              NOT NULL DEFAULT 5,
    [IsActive]        BIT              NOT NULL DEFAULT 1,
    [CreatedAtUtc]    DATETIMEOFFSET(7) NOT NULL DEFAULT SYSDATETIMEOFFSET(),
    [UpdatedAtUtc]    DATETIMEOFFSET(7) NULL
);

CREATE INDEX [IX_WebhookSubscriptions_EventType_IsActive]
    ON [dbo].[WebhookSubscriptions] ([EventType], [IsActive])
    INCLUDE ([TenantId]);

-- Delivery attempts table (outbox)
CREATE TABLE [dbo].[WebhookDeliveryAttempts]
(
    [Id]                 UNIQUEIDENTIFIER NOT NULL PRIMARY KEY DEFAULT NEWID(),
    [SubscriptionId]     UNIQUEIDENTIFIER NOT NULL,
    [TenantId]           NVARCHAR(200)    NULL,
    [EventType]          NVARCHAR(200)    NOT NULL,
    [WebhookUrl]         NVARCHAR(2000)   NOT NULL,
    [HttpMethod]         NVARCHAR(10)     NOT NULL,
    [ContentType]        NVARCHAR(100)    NOT NULL,
    [Headers]            NVARCHAR(MAX)    NULL,
    [TimeoutSeconds]     INT              NOT NULL DEFAULT 30,
    [Payload]            NVARCHAR(MAX)    NOT NULL,
    [State]              INT              NOT NULL DEFAULT 0,
    [RetryCount]         INT              NOT NULL DEFAULT 0,
    [MaxRetries]         INT              NOT NULL DEFAULT 5,
    [NextAttemptAtUtc]   DATETIMEOFFSET(7) NULL,
    [LastError]          NVARCHAR(MAX)    NULL,
    [ResponseStatusCode] INT             NULL,
    [CreatedAtUtc]       DATETIMEOFFSET(7) NOT NULL DEFAULT SYSDATETIMEOFFSET(),
    [DeliveredAtUtc]     DATETIMEOFFSET(7) NULL,
    [LockedUntilUtc]     DATETIMEOFFSET(7) NULL
);

CREATE INDEX [IX_WebhookDeliveryAttempts_State_NextAttempt]
    ON [dbo].[WebhookDeliveryAttempts] ([State], [NextAttemptAtUtc])
    INCLUDE ([LockedUntilUtc], [CreatedAtUtc]);

PostgreSQL Migration

-- Subscriptions table
CREATE TABLE "public"."webhook_subscriptions"
(
    id               UUID         NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id        VARCHAR(200) NULL,
    event_type       VARCHAR(200) NOT NULL,
    name             VARCHAR(200) NOT NULL,
    webhook_url      VARCHAR(2000) NOT NULL,
    http_method      VARCHAR(10)  NOT NULL DEFAULT 'POST',
    content_type     VARCHAR(100) NOT NULL DEFAULT 'application/json',
    headers          JSONB        NULL,
    timeout_seconds  INT          NOT NULL DEFAULT 30,
    max_retries      INT          NOT NULL DEFAULT 5,
    is_active        BOOLEAN      NOT NULL DEFAULT TRUE,
    created_at_utc   TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
    updated_at_utc   TIMESTAMPTZ  NULL
);

CREATE INDEX ix_webhook_subscriptions_event_type_active
    ON "public"."webhook_subscriptions" (event_type, is_active)
    INCLUDE (tenant_id);

-- Delivery attempts table (outbox)
CREATE TABLE "public"."webhook_delivery_attempts"
(
    id                   UUID         NOT NULL PRIMARY KEY DEFAULT gen_random_uuid(),
    subscription_id      UUID         NOT NULL,
    tenant_id            VARCHAR(200) NULL,
    event_type           VARCHAR(200) NOT NULL,
    webhook_url          VARCHAR(2000) NOT NULL,
    http_method          VARCHAR(10)  NOT NULL,
    content_type         VARCHAR(100) NOT NULL,
    headers              JSONB        NULL,
    timeout_seconds      INT          NOT NULL DEFAULT 30,
    payload              TEXT         NOT NULL,
    state                INT          NOT NULL DEFAULT 0,
    retry_count          INT          NOT NULL DEFAULT 0,
    max_retries          INT          NOT NULL DEFAULT 5,
    next_attempt_at_utc  TIMESTAMPTZ  NULL,
    last_error           TEXT         NULL,
    response_status_code INT          NULL,
    created_at_utc       TIMESTAMPTZ  NOT NULL DEFAULT NOW(),
    delivered_at_utc     TIMESTAMPTZ  NULL,
    locked_until_utc     TIMESTAMPTZ  NULL
);

CREATE INDEX ix_webhook_delivery_attempts_state_next_attempt
    ON "public"."webhook_delivery_attempts" (state, next_attempt_at_utc)
    INCLUDE (locked_until_utc, created_at_utc);

Cleanup Recommendations

Old delivered/failed delivery attempts will accumulate over time. Consider running periodic cleanup:

-- SQL Server: Delete delivered attempts older than 30 days
DELETE FROM [dbo].[WebhookDeliveryAttempts]
WHERE [State] = 2 AND [DeliveredAtUtc] < DATEADD(DAY, -30, SYSDATETIMEOFFSET());

-- PostgreSQL: Delete delivered attempts older than 30 days
DELETE FROM "public"."webhook_delivery_attempts"
WHERE state = 2 AND delivered_at_utc < NOW() - INTERVAL '30 days';

License

MIT

Product 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. 
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
0.0.6 64 5/13/2026
0.0.5 76 5/12/2026
0.0.4 102 4/23/2026
0.0.3 96 4/20/2026
0.0.2 94 4/16/2026
0.0.1 108 3/26/2026