When first using Paramore.Brighter, developers often misunderstand how exception handling works, especially around message reprocessing. Unlike many systems, Brighter acknowledges a message after an unhandled exception by default. This article clarifies this behaviour and demonstrates the correct ways to implement retry logic for failed messages in Brighter V9 and V10.
Requirement
- .NET 8 or superior
- Podman or docker
For Brighter, we will need these NuGet packages:
- Paramore.Brighter.MessagingGateway.RocketMQ:
- Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection
- Paramore.Brighter.ServiceActivator.Extensions.Hosting
- Serilog.AspNetCore
For Fluent Brighter:
Brighter Recap
Before diving into handling error, let's review the fundamental Brighter building blocks.
Request (Command/Event)
Messages are simple classes that implement IRequest
.
public class Greeting() : Event(Guid.NewGuid())
{
public string Name { get; set; } = string.Empty;
}
-
Commands: Single-recipient operations (e.g.,
SendEmail
). -
Events: Broadcast notifications (e.g.,
OrderShipped
).
Creating a Message Mapper (optional)
Mappers translate between your .NET objects and Brighter's Message format.
From Brighter V10, this step is optional
public class GreetingMapper : IAmAMessageMapper<Greeting>
{
public Message MapToMessage(Greeting request)
{
var header = new MessageHeader();
header.Id = request.Id;
header.TimeStamp = DateTime.UtcNow;
header.Topic = "greeting.topic"; // The target topic to be publish
header.MessageType = MessageType.MT_EVENT;
var body = new MessageBody(JsonSerializer.Serialize(request));
return new Message(header, body);
}
public Greeting MapToRequest(Message message)
{
return JsonSerializer.Deserialize<Greeting>(message.Body.Bytes)!;
}
}
Implementing a Request Handler
Handlers contain the business logic to process incoming messages.
public class GreetingHandler(ILogger<GreetingHandler> logger) : RequestHandler<Greeting>
{
public override Greeting Handle(Greeting command)
{
logger.LogInformation("Hello {Name}", command.Name);
return base.Handle(command);
}
}
Configuring Fluent Brighter with RocketMQ
The Fluent Brighter API provides a streamlined way to configure your application. The following example sets up a publisher and a subscriber for the Greeting event.
service
.AddHostedService<ServiceActivatorHostedService>()
.AddFluentBrighter(brighter => brighter
.UsingRocketMq(rocket => rocket
.SetConnection(conn => conn
.SetClient(c => c
.SetEndpoints("localhost:8081")
.EnableSsl(false)
.SetRequestTimeout(TimeSpan.FromSeconds(10))
.Build()))
.UsePublications(pub => pub
.AddPublication<GreetingEvent>(p => p
.SetTopic("greeting")))
.UseSubscriptions(sub => sub
.AddSubscription<GreetingEvent>(s => s
.SetSubscriptionName("greeting-sub-name")
.SetTopic("greeting")
.SetConsumerGroup("greeting-consumer-group")
.UseReactorMode()
))));
For a deeper dive into Brighter and RocketMQ, see this detailed article.
Understanding Brighter's Exception Handling
By default, Brighter acknowledges (ACKs) a message after an exception is thrown in a handler. This is because Brighter cannot distinguish between a permanent application error and a transient, recoverable one. The safe default is to assume the error is permanent and not retry, preventing a poison-pill message from blocking the queue.
To explicitly request a retry, you must throw a DeferMessageAction exception. This tells Brighter to negatively acknowledge (NACK) the message, causing the broker to redeliver it.
The Problem
Consider a handler that fails when a Name
property is "fail".
public class GreetingHandler : RequestHandler<Greeting>
{
public override Greeting Handle(Greeting @event)
{
if (@event.Name == "fail")
{
Console.WriteLine("===== fail to process");
throw new Exception("Some error");
}
Console.WriteLine("===== Hello, {0}", @event.Name);
return base.Handle(@event);
}
}
In this scenario, the message with Name = "fail"
is consumed once and then lost because Brighter ACKs it after the Exception is thrown.
Solution 1: Explicit Try/Catch with Defer
The most straightforward method is to wrap your handler logic in a try-catch block and throw DeferMessageAction
for any error you wish to retry.
public class GreetingHandler : RequestHandler<Greeting>
{
public override Greeting Handle(Greeting @event)
{
try
{
if (@event.Name == "fail")
{
Console.WriteLine("===== fail to process");
throw new Exception("Some error");
}
Console.WriteLine("===== Hello, {0}", @event.Name);
return base.Handle(@event);
}
catch (Exception)
{
Console.Write("=== throwing DeferMessageAction");
throw new DeferMessageAction();
}
}
}
Solution 2: Using the Fallback Policy Middleware
A more elegant approach uses Brighter's Russian Doll middleware model. By applying the [FallbackPolicy]
attribute, you can override the Fallback method, which acts as a catch-all when the main Handle method fails.
This is often preferable as it keeps your core business logic clean and separates the retry concern.
public class GreetingHandler : RequestHandler<Greeting>
{
[FallbackPolicy(true, false, 0)]
public override Greeting Handle(Greeting @event)
{
if (@event.Name == "fail")
{
Console.WriteLine("===== fail to process");
throw new Exception("Some error");
}
Console.WriteLine("===== Hello, {0}", @event.Name);
return base.Handle(@event);
}
private static int _counter = 0;
public override Greeting Fallback(Greeting command)
{
var res = Interlocked.Increment(ref _counter);
if (res % 3 == 0)
{
Console.Write("=== marking as completed");
return command;
}
Console.Write("=== throwing DeferMessageAction");
throw new DeferMessageAction();
}
}
Note: For asynchronous handlers, you must use the FallbackPolicyAsync
attribute and override the FallbackAsync
method.
Conclusion
Effectively handling exceptions in Brighter requires understanding its default "assume success" acknowledgement policy. You cannot rely on unhandled exceptions to trigger retries. Instead, you must explicitly control retry behaviour by throwing DeferMessageAction
.
You can implement this either directly within your handler logic using a try-catch block or more cleanly by leveraging the [FallbackPolicy]
middleware to keep your business logic focused and your error handling centralised. The middleware approach is generally recommended for its separation of concerns and greater flexibility.
See the full code: https://github.com/lillo42/brighter-sample/tree/v10-error-handling
Top comments (0)