WebhookKit.EntityFrameworkCore
1.0.0
dotnet add package WebhookKit.EntityFrameworkCore --version 1.0.0
NuGet\Install-Package WebhookKit.EntityFrameworkCore -Version 1.0.0
<PackageReference Include="WebhookKit.EntityFrameworkCore" Version="1.0.0" />
<PackageVersion Include="WebhookKit.EntityFrameworkCore" Version="1.0.0" />
<PackageReference Include="WebhookKit.EntityFrameworkCore" />
paket add WebhookKit.EntityFrameworkCore --version 1.0.0
#r "nuget: WebhookKit.EntityFrameworkCore, 1.0.0"
#:package WebhookKit.EntityFrameworkCore@1.0.0
#addin nuget:?package=WebhookKit.EntityFrameworkCore&version=1.0.0
#tool nuget:?package=WebhookKit.EntityFrameworkCore&version=1.0.0
WebhookKit
Send and receive signed webhooks with retry logic, delivery logging, and signature verification for .NET.
Inspired by spatie/laravel-webhook-server and spatie/laravel-webhook-client.
Table of Contents
- Packages
- Requirements
- Installation
- Quick Start
- Sending Webhooks (Server)
- Receiving Webhooks (Client)
- Storage Backends
- Delivery Log Entity
- Signing Utility
- Configuration Reference
- Multi-Tenancy (ScopeId)
- License
Packages
| NuGet Package | Target Frameworks | Description |
|---|---|---|
WebhookKit |
net8.0, net9.0, net10.0 | Core — signing, dispatcher, delivery entry, options |
WebhookKit.AspNetCore |
net8.0, net10.0 | Middleware and minimal API for receiving webhooks |
WebhookKit.EntityFrameworkCore |
net8.0, net9.0, net10.0 | EF Core delivery log entity configuration and store |
WebhookKit.Dapper |
net8.0, net9.0, net10.0 | Dapper delivery log queries and store |
Requirements
- .NET 8, .NET 9, or .NET 10
- For EF Core storage: any EF Core 9+ relational provider (PostgreSQL, SQL Server, SQLite, etc.)
- For Dapper storage: any
IDbConnectionimplementation
Installation
Install the packages you need via the .NET CLI:
# Core + ASP.NET Core receiver
dotnet add package WebhookKit
dotnet add package WebhookKit.AspNetCore
# EF Core storage
dotnet add package WebhookKit.EntityFrameworkCore
# OR Dapper storage
dotnet add package WebhookKit.Dapper
Quick Start
// Program.cs
builder.Services
.AddWebhookKit(options =>
{
options.Server.MaxRetries = 5;
options.Server.RetryIntervals =
[
TimeSpan.FromSeconds(30),
TimeSpan.FromMinutes(2),
TimeSpan.FromMinutes(15),
TimeSpan.FromHours(1),
TimeSpan.FromHours(6),
];
})
.UseEntityFrameworkCore<AppDbContext>()
.AddHandler<OrderCreatedWebhookHandler>()
.UseRetryWorker();
// In AppDbContext.OnModelCreating:
modelBuilder.ApplyConfiguration(new WebhookDeliveryEntryConfiguration());
Sending Webhooks (Server)
Registration
builder.Services.AddWebhookKit(options =>
{
options.Server.MaxRetries = 5;
options.Server.HttpTimeout = TimeSpan.FromSeconds(30);
options.Server.RetryPollingInterval = TimeSpan.FromSeconds(30);
options.Server.RetryBatchSize = 50;
})
.UseEntityFrameworkCore<AppDbContext>();
Dispatching
Inject IWebhookDispatcher and call DispatchAsync:
public class OrderService(IWebhookDispatcher dispatcher)
{
public async Task CreateOrderAsync(Order order)
{
// ... create order ...
await dispatcher.DispatchAsync(
eventType: "order.created",
payload: new { order.Id, order.Total, order.Status },
endpointUrl: "https://partner.example.com/webhooks",
secret: "endpoint-specific-secret",
scopeId: order.TenantId // optional, for multi-tenant apps
);
}
}
DispatchAsync returns the delivery entry ID so you can track the delivery.
The dispatcher automatically:
- Creates a
WebhookDeliveryEntrywithStatus = "pending" - Signs the payload with HMAC-SHA256
- Sends an HTTP POST with the signature headers
- Updates the entry to
"delivered"on HTTP 2xx, or"failed"with a scheduledNextRetryAton failure
Retry Logic
On delivery failure (non-2xx or network error), the entry is updated to Status = "failed" with NextRetryAt set according to the retry schedule:
| Attempt | Default Delay |
|---|---|
| 1 → 2 | 30 seconds |
| 2 → 3 | 2 minutes |
| 3 → 4 | 15 minutes |
| 4 → 5 | 1 hour |
| 5 → 6 | 6 hours |
After MaxRetries failures the entry moves to Status = "exhausted" with no further retries.
Retry Worker and Secret Resolver
To automatically retry failed deliveries, enable the background worker and provide a secret resolver:
builder.Services
.AddWebhookKit(...)
.UseEntityFrameworkCore<AppDbContext>()
.AddSecretResolver<MySecretResolver>()
.UseRetryWorker();
Implement IWebhookEndpointSecretResolver to return the signing secret for a given endpoint URL:
public class MySecretResolver(IEndpointRepository repo) : IWebhookEndpointSecretResolver
{
public async Task<string?> ResolveAsync(string endpointUrl, string? scopeId, CancellationToken ct)
{
var endpoint = await repo.FindByUrlAsync(endpointUrl, ct);
return endpoint?.Secret;
}
}
Receiving Webhooks (Client)
Registering Handlers
Implement IWebhookHandler for each event type you want to handle:
public class OrderCreatedWebhookHandler : IWebhookHandler
{
public string EventType => "order.created";
public async Task HandleAsync(WebhookPayload payload, CancellationToken ct)
{
var order = payload.Deserialize<OrderDto>();
// process the order...
}
}
// To handle all event types:
public class CatchAllHandler : IWebhookHandler
{
public string EventType => "*";
public Task HandleAsync(WebhookPayload payload, CancellationToken ct)
{
Console.WriteLine($"Received: {payload.EventType} / {payload.DeliveryId}");
return Task.CompletedTask;
}
}
Register via the builder:
builder.Services
.AddWebhookKit(...)
.AddHandler<OrderCreatedWebhookHandler>()
.AddHandler<CatchAllHandler>();
Minimal API Endpoint
Add a typed webhook receiver endpoint using MapWebhookReceiver:
// Program.cs
app.MapWebhookReceiver("/webhooks/receive", secret: "your-endpoint-secret");
This endpoint will:
- Verify the HMAC-SHA256 signature
- Check idempotency via
X-Webhook-Delivery-Id - Dispatch to registered
IWebhookHandlerimplementations
Middleware Approach
For more control, use the middleware:
app.UseWebhookReceiver("/webhooks");
Note: the middleware approach skips automatic signature verification — add your own verification logic before calling IWebhookHandler implementations if needed.
Signature Verification
Inject ISignatureVerifier to manually verify a signature:
public class WebhookController(ISignatureVerifier verifier) : ControllerBase
{
[HttpPost("webhooks")]
public async Task<IActionResult> Receive()
{
var body = await new StreamReader(Request.Body).ReadToEndAsync();
var signature = Request.Headers["X-Webhook-Signature"].FirstOrDefault() ?? string.Empty;
var timestamp = Request.Headers["X-Webhook-Timestamp"].FirstOrDefault() ?? string.Empty;
if (!verifier.Verify(body, "your-secret", signature, timestamp))
return Unauthorized();
// process...
return Ok();
}
}
Idempotency
The minimal API endpoint and middleware automatically deduplicate incoming webhooks using the X-Webhook-Delivery-Id header. If a delivery ID has already been processed (found in IWebhookDeliveryStore), the request returns 200 OK immediately without re-processing.
WebhookPayload
WebhookPayload provides access to the raw body and parsed headers:
public Task HandleAsync(WebhookPayload payload, CancellationToken ct)
{
// Typed deserialization
var dto = payload.Deserialize<MyEventDto>();
// Raw access
var body = payload.RawBody;
var deliveryId = payload.DeliveryId;
var eventType = payload.EventType;
// Case-insensitive header access
var customHeader = payload.Headers["X-My-Header"];
return Task.CompletedTask;
}
Storage Backends
Entity Framework Core
Add WebhookKit.EntityFrameworkCore and call UseEntityFrameworkCore<TContext>:
builder.Services
.AddWebhookKit(...)
.UseEntityFrameworkCore<AppDbContext>();
Apply the entity configuration in your DbContext:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new WebhookDeliveryEntryConfiguration());
// or with a custom table name:
modelBuilder.ApplyConfiguration(new WebhookDeliveryEntryConfiguration("outbox_webhooks"));
}
Generate migrations:
dotnet ef migrations add AddWebhookDeliveries
dotnet ef database update
Dapper
Add WebhookKit.Dapper, register an IDbConnection, and call UseDapper:
builder.Services.AddScoped<IDbConnection>(_ =>
new NpgsqlConnection(connectionString));
builder.Services
.AddWebhookKit(...)
.UseDapper(options => options.TableName = "webhook_deliveries");
Create the table manually (example for PostgreSQL):
CREATE TABLE webhook_deliveries (
id VARCHAR(64) NOT NULL PRIMARY KEY,
endpoint_url VARCHAR(2048) NOT NULL,
event_type VARCHAR(200) NOT NULL,
payload TEXT NOT NULL,
attempt_number INTEGER NOT NULL DEFAULT 1,
http_status INTEGER,
response_body VARCHAR(1024),
error_message TEXT,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
next_retry_at TIMESTAMPTZ,
delivered_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL,
scope_id VARCHAR(64)
);
CREATE INDEX ix_webhook_deliveries_status ON webhook_deliveries (status);
CREATE INDEX ix_webhook_deliveries_status_retry ON webhook_deliveries (status, next_retry_at);
CREATE INDEX ix_webhook_deliveries_scope_id ON webhook_deliveries (scope_id);
CREATE INDEX ix_webhook_deliveries_created_at ON webhook_deliveries (created_at DESC);
Delivery Log Entity
WebhookDeliveryEntry tracks every delivery attempt:
public class WebhookDeliveryEntry
{
public string Id { get; set; } // Guid("N") — also used as X-Webhook-Delivery-Id
public string EndpointUrl { get; set; }
public string EventType { get; set; }
public string Payload { get; set; } // JSON string
public int AttemptNumber { get; set; } // starts at 1
public int? HttpStatus { get; set; }
public string? ResponseBody { get; set; } // truncated to 1 KB
public string? ErrorMessage { get; set; }
public string Status { get; set; } // pending | delivered | failed | exhausted
public DateTimeOffset? NextRetryAt { get; set; }
public DateTimeOffset? DeliveredAt { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public string? ScopeId { get; set; }
}
Status values are available as constants on WebhookDeliveryStatus:
WebhookDeliveryStatus.Pending // "pending"
WebhookDeliveryStatus.Delivered // "delivered"
WebhookDeliveryStatus.Failed // "failed"
WebhookDeliveryStatus.Exhausted // "exhausted"
Signing Utility
WebhookSigner is a static utility for HMAC-SHA256 signing and verification:
// Sign a payload
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var signature = WebhookSigner.Sign(payload, secret, timestamp);
// Verify an incoming signature (default: 5 minute replay window)
bool isValid = WebhookSigner.Verify(payload, secret, signature, timestamp);
// Verify with a custom replay window
bool isValid = WebhookSigner.Verify(payload, secret, signature, timestamp, maxAge: TimeSpan.FromMinutes(10));
// Disable replay protection entirely
bool isValid = WebhookSigner.Verify(payload, secret, signature, timestamp, maxAge: null);
The signed string format is {timestamp}.{payload}, matching the pattern used by Stripe and similar webhook providers.
HTTP Headers
Outbound webhooks include:
| Header | Value |
|---|---|
X-Webhook-Signature |
HMAC-SHA256 hex signature |
X-Webhook-Timestamp |
Unix timestamp (seconds) |
X-Webhook-Delivery-Id |
Unique delivery ID (Guid "N") |
X-Webhook-Event |
Event type string |
Configuration Reference
builder.Services.AddWebhookKit(options =>
{
// Server (outbound) options
options.Server.MaxRetries = 5;
options.Server.RetryIntervals =
[
TimeSpan.FromSeconds(30),
TimeSpan.FromMinutes(2),
TimeSpan.FromMinutes(15),
TimeSpan.FromHours(1),
TimeSpan.FromHours(6),
];
options.Server.HttpTimeout = TimeSpan.FromSeconds(30);
options.Server.RetryPollingInterval = TimeSpan.FromSeconds(30);
options.Server.RetryBatchSize = 50;
options.Server.HttpClientName = "WebhookKit"; // named HttpClient
// Client (inbound) options
options.Client.MaxRequestAge = TimeSpan.FromMinutes(5); // null = disable replay protection
});
Multi-Tenancy (ScopeId)
Pass a scopeId to associate a delivery with a tenant or scope:
await dispatcher.DispatchAsync(
eventType: "order.created",
payload: orderDto,
endpointUrl: tenant.WebhookUrl,
secret: tenant.WebhookSecret,
scopeId: tenant.Id
);
The ScopeId is stored on WebhookDeliveryEntry and passed to IWebhookEndpointSecretResolver.ResolveAsync for multi-tenant secret resolution.
License
MIT — see LICENSE for details.
| 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 is compatible. 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 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
- Microsoft.EntityFrameworkCore.Relational (>= 10.0.3)
- WebhookKit (>= 1.0.0)
-
net8.0
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.3)
- WebhookKit (>= 1.0.0)
-
net9.0
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.3)
- WebhookKit (>= 1.0.0)
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.0.0 | 106 | 3/26/2026 |