Skip to main content
Version: v1.2.7 (latest)

In-Process Dead-Letter Handling

The lightest integration is AddDeadLetter(...). It returns a DeadLetterBuilder that can register a callback or handler receiving DeadLetterContext whenever a channel throws while publishing.

Registration

With Inline Handler

using Hermodr;
using Microsoft.Extensions.DependencyInjection;

builder.Services
.AddEventPublisher(options =>
{
options.Source = new Uri("https://orders.example.com");
options.ThrowOnErrors = false;
})
.AddRabbitMq(options =>
{
options.ConnectionString = "amqp://guest:guest@localhost:5672";
options.ExchangeName = "orders";
})
.AddDeadLetter(deadLetter => deadLetter.UseHandler(context =>
{
Console.WriteLine(
$"Dead-lettered {context.Event.Type} " +
$"from channel {context.ChannelName ?? context.ChannelType?.Name}: " +
$"{context.Exception.Message}");
}));

With Handler Class

public class LoggingDeadLetterHandler : IDeadLetterHandler
{
private readonly ILogger<LoggingDeadLetterHandler> _logger;

public LoggingDeadLetterHandler(ILogger<LoggingDeadLetterHandler> logger)
=> _logger = logger;

public Task HandleAsync(DeadLetterContext context, CancellationToken ct)
{
_logger.LogError(
context.Exception,
"Dead-lettered event {EventType} from channel {Channel}: {Error}",
context.Event.Type,
context.ChannelName ?? context.ChannelType?.Name,
context.Exception.Message);

return Task.CompletedTask;
}
}

// Registration
builder.Services
.AddEventPublisher()
.AddRabbitMq(/* config */)
.AddDeadLetter()
.UseHandler<LoggingDeadLetterHandler>();

DeadLetterContext

DeadLetterContext gives your handler the information you usually need for logging, fallback delivery, or persistence:

PropertyMeaning
PublisherNameName of the publisher pipeline that failed
EventThe CloudEvent whose delivery failed
ExceptionThe thrown exception
ServicesScoped service provider for the current publish attempt
CancellationTokenPublish cancellation token
OptionsEffective per-call publish options
ChannelTypeConcrete channel type involved in the failure
ChannelNameLogical channel name, when available

Behavior with ThrowOnErrors

Dead-letter handling does not hide the original publish outcome:

ScenarioBehavior
ThrowOnErrors = falseHandler runs, but publish call does not throw
ThrowOnErrors = trueHandler runs before the publisher throws EventPublishException
Handler itself throwsPublish call fails regardless of ThrowOnErrors

Immediate Replay Through the Pipeline

You can replay a dead-lettered event immediately by publishing the captured CloudEvent again from inside the handler.

DeadLetterReplayPublishOptions marks the second publish as a replay attempt, so a replay failure is not stored again as a brand-new dead letter.

using Hermodr;
using Microsoft.Extensions.DependencyInjection;

builder.Services
.AddEventPublisher(options =>
{
options.Source = new Uri("https://orders.example.com");
options.ThrowOnErrors = true;
})
.AddChannel<PrimaryChannel>(channelName: "primary")
.AddChannel<RecoveryChannel>(channelName: "recovery")
.AddDeadLetter(deadLetter => deadLetter.UseHandler(async context =>
{
var publisher = context.Services.GetRequiredService<IEventPublisher>();

await publisher.PublishEventAsync(
context.Event,
new DeadLetterReplayPublishOptions(
new NamedChannelPublishOptions("recovery")),
context.CancellationToken);
}));

This pattern is useful when:

  • The fallback channel lives in the same process
  • You want immediate rerouting instead of durable storage
  • You want to keep business code unaware of the fallback logic