RepletoryLib.Webhook.Core
1.0.0
dotnet add package RepletoryLib.Webhook.Core --version 1.0.0
NuGet\Install-Package RepletoryLib.Webhook.Core -Version 1.0.0
<PackageReference Include="RepletoryLib.Webhook.Core" Version="1.0.0" />
<PackageVersion Include="RepletoryLib.Webhook.Core" Version="1.0.0" />
<PackageReference Include="RepletoryLib.Webhook.Core" />
paket add RepletoryLib.Webhook.Core --version 1.0.0
#r "nuget: RepletoryLib.Webhook.Core, 1.0.0"
#:package RepletoryLib.Webhook.Core@1.0.0
#addin nuget:?package=RepletoryLib.Webhook.Core&version=1.0.0
#tool nuget:?package=RepletoryLib.Webhook.Core&version=1.0.0
RepletoryLib.Webhook.Core
Webhook sender and receiver implementation with HMAC-SHA256 signature validation, HTTP delivery, retry logic, in-memory subscription management, and ASP.NET Core middleware for RepletoryLib.
Part of the RepletoryLib ecosystem -- standalone, reusable .NET 10 libraries with zero business logic.
Overview
RepletoryLib.Webhook.Core provides the default implementation of all webhook abstractions defined in RepletoryLib.Webhook.Abstractions. It includes:
HttpWebhookSender-- HTTP-based outbound webhook delivery with typedHttpClient, HMAC signature generation, custom headers, and bulk send supportHmacSignatureValidator-- HMAC-SHA256 signature generation and constant-time validation to prevent timing attacksInMemoryWebhookSubscriptionManager-- Thread-safe,ConcurrentDictionary-backed subscription storage for development, testing, and single-instance deploymentsWebhookReceiver-- Inbound webhook processor that validates signatures and dispatches events to registered handlersWebhookEventRouter-- Routes incoming webhooks to matchingIWebhookEventHandler<string>instances by event type or wildcardWebhookReceiverMiddleware-- ASP.NET Core middleware that exposes a POST endpoint for receiving inbound webhooks with method, body size, and signature validation
One call to AddRepletoryWebhook(configuration) registers everything. One call to UseRepletoryWebhookReceiver() maps the receiver endpoint.
Key Features
- Single-line DI registration --
AddRepletoryWebhookregisters all services, options, and the typed HTTP client - Single-line endpoint mapping --
UseRepletoryWebhookReceivermaps the configurable POST endpoint - HMAC-SHA256 signatures -- Payloads are signed with
sha256={hex}format using constant-time comparison - HTTP delivery -- Outbound webhooks delivered via typed
HttpClientwith configurable timeout and User-Agent - Bulk delivery -- Send to multiple endpoints with per-delivery result tracking
- Retry support -- Retry previously failed deliveries with incremented attempt numbers
- In-memory subscriptions -- Thread-safe subscription management with wildcard (
*) event matching - Event routing -- Incoming webhooks automatically dispatched to matching
IWebhookEventHandler<string>instances - Request validation -- Middleware enforces POST method, max body size (HTTP 413), and signature validation (HTTP 401)
- Configurable options -- Extend
WebhookOptionswithWebhookCoreOptionsfor receiver path, body size limits, and User-Agent
Installation
dotnet add package RepletoryLib.Webhook.Core
Or add to your .csproj:
<PackageReference Include="RepletoryLib.Webhook.Core" Version="1.0.0" />
Note: RepletoryLib packages are published to a local BaGet feed. See the main repository README for feed configuration.
Dependencies
| Package | Type |
|---|---|
RepletoryLib.Webhook.Abstractions |
RepletoryLib |
Microsoft.Extensions.Http |
NuGet |
Microsoft.Extensions.Options.ConfigurationExtensions |
NuGet |
Microsoft.AspNetCore.App |
Framework reference |
Quick Start
Complete Program.cs Setup
using RepletoryLib.Webhook.Core;
using RepletoryLib.Webhook.Abstractions.Interfaces;
var builder = WebApplication.CreateBuilder(args);
// Register all webhook services (sender, receiver, signature validator,
// subscription manager, event router, and middleware)
builder.Services.AddRepletoryWebhook(builder.Configuration);
// Register your custom event handlers
builder.Services.AddScoped<IWebhookEventHandler<string>, OrderCreatedHandler>();
builder.Services.AddScoped<IWebhookEventHandler<string>, PaymentReceivedHandler>();
var app = builder.Build();
// Map the webhook receiver POST endpoint (default: /webhooks)
app.UseRepletoryWebhookReceiver();
app.Run();
appsettings.json
{
"Webhook": {
"SecretKey": "your-shared-secret-key",
"SignatureHeaderName": "X-Webhook-Signature",
"EventTypeHeaderName": "X-Webhook-Event",
"RetryCount": 3,
"RetryDelaySeconds": 5,
"TimeoutSeconds": 30,
"Core": {
"ReceiverEndpointPath": "/webhooks",
"MaxRequestBodySizeBytes": 1048576,
"UserAgent": "RepletoryLib-Webhook/1.0"
}
}
}
Configuration
WebhookOptions (base)
| Property | Type | Default | Description |
|---|---|---|---|
SecretKey |
string |
"" |
Secret key used to sign and validate webhook payloads |
SignatureHeaderName |
string |
"X-Webhook-Signature" |
HTTP header name for transmitting the webhook signature |
EventTypeHeaderName |
string |
"X-Webhook-Event" |
HTTP header name for transmitting the webhook event type |
RetryCount |
int |
3 |
Maximum number of retry attempts for failed deliveries |
RetryDelaySeconds |
int |
5 |
Delay in seconds between retry attempts |
TimeoutSeconds |
int |
30 |
Timeout in seconds for each webhook delivery HTTP request |
Section name: "Webhook"
WebhookCoreOptions (extends WebhookOptions)
| Property | Type | Default | Description |
|---|---|---|---|
ReceiverEndpointPath |
string |
"/webhooks" |
The endpoint path on which the webhook receiver listens for incoming requests |
MaxRequestBodySizeBytes |
int |
1048576 (1 MB) |
Maximum allowed request body size in bytes; requests exceeding this are rejected with HTTP 413 |
UserAgent |
string |
"RepletoryLib-Webhook/1.0" |
User-Agent header value used when sending outbound webhook requests |
Section name: "Webhook:Core"
Usage Examples
DI Registration
using RepletoryLib.Webhook.Core;
// Register all webhook services with a single call
builder.Services.AddRepletoryWebhook(builder.Configuration);
This registers the following services:
| Interface / Service | Implementation | Lifetime |
|---|---|---|
IWebhookSender |
HttpWebhookSender |
Transient (via AddHttpClient) |
IWebhookSignatureValidator |
HmacSignatureValidator |
Singleton |
IWebhookSubscriptionManager |
InMemoryWebhookSubscriptionManager |
Singleton |
IWebhookReceiver |
WebhookReceiver |
Scoped |
WebhookEventRouter |
WebhookEventRouter |
Scoped |
WebhookReceiverMiddleware |
WebhookReceiverMiddleware |
Scoped |
Mapping the Receiver Endpoint
using RepletoryLib.Webhook.Core;
var app = builder.Build();
// Maps a POST endpoint at the configured path (default: /webhooks)
app.UseRepletoryWebhookReceiver();
app.Run();
The middleware handles:
- 405 Method Not Allowed -- Non-POST requests
- 413 Payload Too Large -- Body exceeds
MaxRequestBodySizeBytes - 401 Unauthorized -- Invalid or missing signature (when
SecretKeyis configured) - 400 Bad Request -- Handler processing errors
- 200 OK -- Webhook processed successfully (returns
{"status":"ok"})
Sending Outbound Webhooks
using RepletoryLib.Webhook.Abstractions.Interfaces;
using RepletoryLib.Webhook.Abstractions.Models.Outbound;
public class NotificationBroadcaster
{
private readonly IWebhookSender _sender;
private readonly IWebhookSubscriptionManager _subscriptions;
public NotificationBroadcaster(
IWebhookSender sender,
IWebhookSubscriptionManager subscriptions)
{
_sender = sender;
_subscriptions = subscriptions;
}
public async Task BroadcastAsync(string eventType, string data)
{
var subscribers = await _subscriptions.GetByEventTypeAsync(eventType);
var requests = subscribers.Where(s => s.IsActive).Select(sub => new WebhookDeliveryRequest
{
Url = sub.Url,
Secret = sub.Secret,
Payload = new WebhookPayload
{
EventType = eventType,
Data = data
}
});
var result = await _sender.SendBulkAsync(requests);
Console.WriteLine($"Delivered: {result.SuccessCount}, Failed: {result.FailureCount}");
}
}
Implementing a Custom Event Handler
using RepletoryLib.Webhook.Abstractions.Interfaces;
using RepletoryLib.Webhook.Abstractions.Models.Inbound;
using System.Text.Json;
public class OrderCreatedHandler : IWebhookEventHandler<string>
{
private readonly ILogger<OrderCreatedHandler> _logger;
public OrderCreatedHandler(ILogger<OrderCreatedHandler> logger) => _logger = logger;
public string EventType => "order.created";
public async Task HandleAsync(string payload, IncomingWebhook webhook, CancellationToken ct = default)
{
var order = JsonSerializer.Deserialize<OrderDto>(payload);
_logger.LogInformation(
"Processing order {OrderId} received at {ReceivedAt} from {SourceIp}",
order?.Id, webhook.ReceivedAt, webhook.SourceIp);
// Process the order...
await ProcessOrderAsync(order!, ct);
}
private Task ProcessOrderAsync(OrderDto order, CancellationToken ct)
{
// Your business logic here
return Task.CompletedTask;
}
}
public record OrderDto(string Id, string CustomerName, decimal Total);
Register the handler in Program.cs:
builder.Services.AddScoped<IWebhookEventHandler<string>, OrderCreatedHandler>();
Implementing Multiple Event Handlers
// Handler for payment events
public class PaymentReceivedHandler : IWebhookEventHandler<string>
{
public string EventType => "payment.received";
public async Task HandleAsync(string payload, IncomingWebhook webhook, CancellationToken ct = default)
{
var payment = JsonSerializer.Deserialize<PaymentDto>(payload);
await UpdatePaymentStatusAsync(payment!);
}
private Task UpdatePaymentStatusAsync(PaymentDto payment) => Task.CompletedTask;
}
// Wildcard handler that logs all events
public class WebhookAuditHandler : IWebhookEventHandler<string>
{
private readonly ILogger<WebhookAuditHandler> _logger;
public WebhookAuditHandler(ILogger<WebhookAuditHandler> logger) => _logger = logger;
public string EventType => "*";
public async Task HandleAsync(string payload, IncomingWebhook webhook, CancellationToken ct = default)
{
_logger.LogInformation(
"Audit: EventType={EventType}, SourceIp={SourceIp}, ReceivedAt={ReceivedAt}, BodyLength={BodyLength}",
webhook.EventType, webhook.SourceIp, webhook.ReceivedAt, webhook.Body.Length);
await Task.CompletedTask;
}
}
// Register all handlers
builder.Services.AddScoped<IWebhookEventHandler<string>, OrderCreatedHandler>();
builder.Services.AddScoped<IWebhookEventHandler<string>, PaymentReceivedHandler>();
builder.Services.AddScoped<IWebhookEventHandler<string>, WebhookAuditHandler>();
HMAC Signature Validation
The HmacSignatureValidator generates and validates HMAC-SHA256 signatures in the format sha256={hex}:
using RepletoryLib.Webhook.Abstractions.Interfaces;
public class SignatureExample
{
private readonly IWebhookSignatureValidator _validator;
public SignatureExample(IWebhookSignatureValidator validator) => _validator = validator;
public void Demo()
{
var payload = "{\"event\":\"order.created\",\"data\":{\"id\":\"123\"}}";
var secret = "my-secret-key";
// Generate a signature
string signature = _validator.GenerateSignature(payload, secret);
// Result: "sha256=a1b2c3d4..."
// Validate a signature (uses constant-time comparison)
bool isValid = _validator.ValidateSignature(payload, signature, secret);
// Result: true
}
}
Managing Subscriptions
The InMemoryWebhookSubscriptionManager uses a ConcurrentDictionary for thread-safe subscription storage:
using RepletoryLib.Webhook.Abstractions.Interfaces;
using RepletoryLib.Webhook.Abstractions.Models.Outbound;
public class SubscriptionController
{
private readonly IWebhookSubscriptionManager _manager;
public SubscriptionController(IWebhookSubscriptionManager manager) => _manager = manager;
public async Task ManageSubscriptionsAsync()
{
// Create a subscription for a specific event type
var sub = await _manager.SubscribeAsync(new WebhookSubscription
{
Url = "https://partner.example.com/webhooks",
EventType = "order.created",
Secret = "partner-shared-secret"
});
Console.WriteLine($"Subscription created: {sub.Id}");
// Create a wildcard subscription
await _manager.SubscribeAsync(new WebhookSubscription
{
Url = "https://audit.example.com/webhooks",
EventType = "*",
Secret = "audit-secret"
});
// Get all subscriptions for an event type (includes wildcard matches)
var subs = await _manager.GetByEventTypeAsync("order.created");
Console.WriteLine($"Matching subscriptions: {subs.Count}");
// Deactivate a subscription
await _manager.SetActiveAsync(sub.Id, isActive: false);
// Retrieve a subscription by ID
var fetched = await _manager.GetByIdAsync(sub.Id);
Console.WriteLine($"Active: {fetched?.IsActive}"); // false
// List all subscriptions
var all = await _manager.GetAllAsync();
Console.WriteLine($"Total subscriptions: {all.Count}");
// Remove a subscription
await _manager.UnsubscribeAsync(sub.Id);
}
}
Retrying Failed Deliveries
using RepletoryLib.Webhook.Abstractions.Interfaces;
using RepletoryLib.Webhook.Abstractions.Models.Outbound;
public class WebhookRetryService
{
private readonly IWebhookSender _sender;
private readonly ILogger<WebhookRetryService> _logger;
public WebhookRetryService(IWebhookSender sender, ILogger<WebhookRetryService> logger)
{
_sender = sender;
_logger = logger;
}
public async Task<WebhookDeliveryResult> SendWithRetryAsync(
WebhookDeliveryRequest request, int maxRetries = 3)
{
var result = await _sender.SendAsync(request);
while (!result.Success && request.AttemptNumber < maxRetries)
{
request.AttemptNumber++;
_logger.LogInformation(
"Retrying webhook to {Url} (attempt {Attempt}/{Max})",
request.Url, request.AttemptNumber, maxRetries);
result = await _sender.RetryAsync(request);
}
return result;
}
}
Complete End-to-End Example
Program.cs:
using RepletoryLib.Webhook.Core;
using RepletoryLib.Webhook.Abstractions.Interfaces;
var builder = WebApplication.CreateBuilder(args);
// Register webhook services
builder.Services.AddRepletoryWebhook(builder.Configuration);
// Register event handlers
builder.Services.AddScoped<IWebhookEventHandler<string>, OrderCreatedHandler>();
builder.Services.AddScoped<IWebhookEventHandler<string>, PaymentReceivedHandler>();
builder.Services.AddScoped<IWebhookEventHandler<string>, WebhookAuditHandler>();
var app = builder.Build();
// Map the webhook receiver endpoint
app.UseRepletoryWebhookReceiver();
// Example: register a subscription on startup
using (var scope = app.Services.CreateScope())
{
var manager = scope.ServiceProvider.GetRequiredService<IWebhookSubscriptionManager>();
await manager.SubscribeAsync(new RepletoryLib.Webhook.Abstractions.Models.Outbound.WebhookSubscription
{
Url = "https://partner.example.com/webhooks",
EventType = "order.created",
Secret = "partner-secret"
});
}
app.Run();
appsettings.json:
{
"Webhook": {
"SecretKey": "my-webhook-signing-secret",
"SignatureHeaderName": "X-Webhook-Signature",
"EventTypeHeaderName": "X-Webhook-Event",
"RetryCount": 3,
"RetryDelaySeconds": 5,
"TimeoutSeconds": 30,
"Core": {
"ReceiverEndpointPath": "/api/webhooks",
"MaxRequestBodySizeBytes": 2097152,
"UserAgent": "MyApp-Webhook/1.0"
}
}
}
API Reference
Extension Methods
ServiceCollectionExtensions
| Method | Returns | Description |
|---|---|---|
AddRepletoryWebhook(services, configuration) |
IServiceCollection |
Registers all webhook core services, options, and the typed HTTP client |
WebApplicationExtensions
| Method | Returns | Description |
|---|---|---|
UseRepletoryWebhookReceiver(app) |
WebApplication |
Maps the webhook receiver POST endpoint at the configured path |
Services
HttpWebhookSender
Implements IWebhookSender. Delivers webhook payloads via typed HttpClient with:
- JSON serialization of
WebhookPayload - HMAC-SHA256 signature generation when
Secretis provided on the request - Event type header (
X-Webhook-Event) set automatically - Custom headers from
WebhookDeliveryRequest.Headersforwarded to the target - Configurable timeout from
WebhookOptions.TimeoutSeconds - Configurable User-Agent from
WebhookCoreOptions.UserAgent
HmacSignatureValidator
Implements IWebhookSignatureValidator. Generates and validates HMAC-SHA256 signatures:
- Signature format:
sha256={lowercase-hex} - Uses
HMACSHA256.HashDatafor signature generation - Uses
CryptographicOperations.FixedTimeEqualsfor constant-time comparison to prevent timing attacks
InMemoryWebhookSubscriptionManager
Implements IWebhookSubscriptionManager. Thread-safe subscription storage using ConcurrentDictionary:
- Assigns new
GuidID andCreatedAttimestamp onSubscribeAsync GetByEventTypeAsyncreturns subscriptions matching the event type or wildcard (*)- Suitable for development, testing, and single-instance deployments
- For production multi-instance deployments, implement
IWebhookSubscriptionManagerwith a persistent store
WebhookReceiver
Implements IWebhookReceiver. Processes incoming webhooks:
- Validates signature when
SecretKeyis configured inWebhookOptions - Returns
WebhookValidationResult.Failed("Missing signature")when signature is required but absent - Returns
WebhookValidationResult.Failed("Invalid signature")when signature does not match - Dispatches to
WebhookEventRouterafter successful validation - Catches handler exceptions and returns failure with
SignatureValid = true
WebhookEventRouter
Routes incoming webhooks to registered IWebhookEventHandler<string> instances:
- Resolves handlers from DI via
IServiceProvider - Matches handlers by exact event type or wildcard (
*) - Invokes all matching handlers sequentially
WebhookReceiverMiddleware
ASP.NET Core middleware for the webhook receiver endpoint:
- Validates request method is POST (405 otherwise)
- Validates body size against
MaxRequestBodySizeBytes(413 otherwise) - Reads body, extracts signature and event type from configured headers
- Constructs
IncomingWebhookwith body, signature, headers, timestamp, and source IP - Delegates to
IWebhookReceiver.ProcessAsync - Returns JSON responses:
{"status":"ok"}on success,{"error":"..."}on failure
Options
WebhookCoreOptions
Extends WebhookOptions with receiver-specific and HTTP client settings.
| Property | Type | Default | Description |
|---|---|---|---|
ReceiverEndpointPath |
string |
"/webhooks" |
POST endpoint path for the webhook receiver |
MaxRequestBodySizeBytes |
int |
1048576 (1 MB) |
Maximum allowed request body size; rejects with HTTP 413 |
UserAgent |
string |
"RepletoryLib-Webhook/1.0" |
User-Agent header for outbound requests |
Section name: "Webhook:Core"
Integration with Other RepletoryLib Packages
| Package | Relationship |
|---|---|
RepletoryLib.Webhook.Abstractions |
Direct dependency -- interfaces, models, enums, and base options |
RepletoryLib.Common |
Transitive dependency -- shared base types |
Testing
using NSubstitute;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using RepletoryLib.Webhook.Abstractions.Interfaces;
using RepletoryLib.Webhook.Abstractions.Models.Inbound;
using RepletoryLib.Webhook.Abstractions.Options;
using RepletoryLib.Webhook.Core.Services;
public class WebhookReceiverTests
{
[Fact]
public async Task ProcessAsync_returns_success_when_signature_is_valid()
{
// Arrange
var validator = new HmacSignatureValidator();
var secret = "test-secret";
var body = "{\"event\":\"order.created\"}";
var signature = validator.GenerateSignature(body, secret);
var options = Options.Create(new WebhookOptions { SecretKey = secret });
var router = Substitute.For<WebhookEventRouter>(
Substitute.For<IServiceProvider>(),
Substitute.For<ILogger<WebhookEventRouter>>());
var receiver = new WebhookReceiver(
validator, router, options, Substitute.For<ILogger<WebhookReceiver>>());
var webhook = new IncomingWebhook
{
EventType = "order.created",
Body = body,
Signature = signature
};
// Act
var result = await receiver.ProcessAsync(webhook);
// Assert
result.Success.Should().BeTrue();
result.SignatureValid.Should().BeTrue();
result.EventType.Should().Be("order.created");
}
[Fact]
public async Task ProcessAsync_returns_failure_when_signature_is_invalid()
{
// Arrange
var validator = new HmacSignatureValidator();
var options = Options.Create(new WebhookOptions { SecretKey = "correct-secret" });
var router = Substitute.For<WebhookEventRouter>(
Substitute.For<IServiceProvider>(),
Substitute.For<ILogger<WebhookEventRouter>>());
var receiver = new WebhookReceiver(
validator, router, options, Substitute.For<ILogger<WebhookReceiver>>());
var webhook = new IncomingWebhook
{
EventType = "order.created",
Body = "{\"event\":\"order.created\"}",
Signature = "sha256=invalid"
};
// Act
var result = await receiver.ProcessAsync(webhook);
// Assert
result.Success.Should().BeFalse();
result.SignatureValid.Should().BeFalse();
result.Error.Should().Be("Invalid signature");
}
}
public class HmacSignatureValidatorTests
{
[Fact]
public void GenerateSignature_returns_sha256_prefixed_hex()
{
var validator = new HmacSignatureValidator();
var signature = validator.GenerateSignature("test-payload", "test-secret");
signature.Should().StartWith("sha256=");
}
[Fact]
public void ValidateSignature_returns_true_for_matching_signature()
{
var validator = new HmacSignatureValidator();
var payload = "{\"event\":\"test\"}";
var secret = "my-secret";
var signature = validator.GenerateSignature(payload, secret);
var isValid = validator.ValidateSignature(payload, signature, secret);
isValid.Should().BeTrue();
}
[Fact]
public void ValidateSignature_returns_false_for_wrong_secret()
{
var validator = new HmacSignatureValidator();
var payload = "{\"event\":\"test\"}";
var signature = validator.GenerateSignature(payload, "correct-secret");
var isValid = validator.ValidateSignature(payload, signature, "wrong-secret");
isValid.Should().BeFalse();
}
}
Troubleshooting
| Issue | Solution |
|---|---|
| Receiver returns 405 | Ensure the client sends a POST request to the configured ReceiverEndpointPath |
| Receiver returns 413 | The request body exceeds MaxRequestBodySizeBytes; increase the limit or reduce the payload size |
| Receiver returns 401 | The signature is missing or invalid; verify that the sender uses the same SecretKey and SignatureHeaderName |
| Receiver returns 400 | An event handler threw an exception; check application logs for the handler error |
| Outbound delivery times out | Increase TimeoutSeconds in WebhookOptions or investigate the target endpoint's response time |
IWebhookEventHandler<string> not invoked |
Ensure the handler is registered in DI and its EventType property matches the incoming event type or "*" |
| Subscriptions lost after restart | InMemoryWebhookSubscriptionManager does not persist data; implement IWebhookSubscriptionManager with a database for production |
License
This project is licensed under the MIT License.
Copyright (c) 2024-2026 Repletory.
For complete documentation, infrastructure setup, and configuration reference, see the RepletoryLib main repository.
| Product | Versions 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. |
-
net10.0
- RepletoryLib.Webhook.Abstractions (>= 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 | 66 | 3/2/2026 |