Skip to main content
Version: Next

Dead-Letter Persistence

Storage is a dead-letter concern on its own: you can persist failed deliveries even if you don't enable replay yet.

Storage Contracts

ContractPurpose
IDeadLetterMessagePersisted dead-letter payload plus replay metadata
IDeadLetterMessageFactory<TMessage>Creates a stored message from DeadLetterContext
IDeadLetterMessageStorePersists messages and manages replay state transitions
IDeadLetterMessageReplayerReplays a stored message through a publisher pipeline
IDeadLetterReplayProcessorProcesses pending messages in batches

Registration with Custom Store

services.AddEventPublisher(options =>
{
options.Source = new Uri("https://orders.example.com");
options.ThrowOnErrors = false;
})
.AddRabbitMq(options => { /* transport config */ })
.AddDeadLetter()
.UseRepository<MyDeadLetterMessage, MyDeadLetterStore>()
.WithFactory<MyDeadLetterMessage, MyDeadLetterMessageFactory>();

AddDeadLetter() exposes storage as a dead-letter sub-feature. Use UseRepository<TMessage, TStore>() plus WithFactory<TMessage, TFactory>() when you want to persist failed deliveries with a custom storage model, even if replay is not enabled.

The dead-letter handler converts DeadLetterContext into TMessage and saves it through the configured IDeadLetterMessageStore.

IDeadLetterMessage

Every persisted message exposes:

  • The original CloudEvent
  • The originating publisher name
  • Channel metadata (ChannelName, ChannelType)
  • The last error message
  • Replay state (Status, ReplayCount, NextReplayAt)

Example Custom Message

using CloudNative.CloudEvents;
using Hermodr.Publisher.DeadLetter;

public class MyDeadLetterMessage : IDeadLetterMessage
{
public string Id { get; set; } = Guid.NewGuid().ToString("N");

public CloudEvent Event { get; set; } = default!;
public string PublisherName { get; set; } = default!;
public string? ChannelName { get; set; }
public Type? ChannelType { get; set; }
public string ErrorMessage { get; set; } = default!;

public DeadLetterMessageStatus Status { get; set; } = DeadLetterMessageStatus.Pending;
public int ReplayCount { get; set; }
public DateTimeOffset? NextReplayAt { get; set; }
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset LastStatusAt { get; set; } = DateTimeOffset.UtcNow;
}

Timing Metadata in Dead-Letter Records

Dead-letter records intentionally separate event history from replay workflow timing:

FieldMeaning
EventTime / CloudEvent.timeWhen the original event occurred
CreatedAtWhen the dead-letter record was created
LastStatusAtWhen replay status last changed (Replaying, Replayed, Failed)
NextReplayAtEarliest time the next replay attempt is allowed

NextReplayAt and LastStatusAt are operational scheduling markers, not business timestamps. Replay components compute them through the configured system-time abstraction (IEventSystemTime) so tests can freeze time and assert exact values.