Brighter is a feature-rich .NET library for dispatching and processing commands that make it easy to implement in-process and out-of-process processing; moreover, it makes it easy to implement CQRS architecture. In this article, we will learn how to use Brighter in .NET and talk about Brighter’s different features and how we can implement them.
Let’s start.
What Is CQRS?
Firstly, we need to talk about CQRS (Command Query Responsibility Segregation). It is a software architecture pattern that separates the models for reading and writing data. Like most approaches in software development, this is not bulletproof; however, the separation of read and write makes it easy to develop high-performance applications that comprise complex business logic.
Brighter in .NET and In-Process Command Processing
Brighter is a library that allows us to create CQRS-based applications. One form of command processing Brighter enables for us is in-process handling, which can either be synchronous or asynchronous.
To use the core functionalities of Brighter, we need to install the Paramore.Brighter
NuGet package:
dotnet add package Paramore.Brighter
Next, the Paramore.Brighter.Extensions.DependencyInjection
package allows us to conveniently configure dependency injection for Brighter. Therefore, we will install it as well:
dotnet add package Paramore.Brighter.Extensions.DependencyInjection
We have all the necessary packages for this section; next, let’s try to understand Brighter command and command processors.
Commands With Brighter in .NET
In Brighter, an instruction to perform a task is called a command. Moreover, command results in a change of state. It is important to note that there is a one-to-one relationship between a command and a command processor. Let’s create a simple command:
public class PingCommand() : Command(Guid.NewGuid());
To create a command, we simply define a class that extends from the Command
class. The Command
class is from the Brighter package, and it has one constructor that takes one argument, which is of type Guid
, where we use the NewGuid()
static method to create a new Guid
.
Synchronous Brighter Command Handler
We can use a synchronous command handler in scenarios where we simply want to execute synchronous code. Let’s see this in action:
public class PingCommandHandler : RequestHandler<PingCommand> { public override PingCommand Handle(PingCommand command) { Console.WriteLine("[PingCommandHandler] >> Pong"); return base.Handle(command); } }
Here, we define PingCommandHandler
class that extends RequestHandler
class that comes from Brighter. The Handle()
method inside the PingCommandHandler
class will be executed when dispatching the PingCommand
command. This is a simple example that demonstrates the implementation of a synchronous command processor. Hence, all we have inside the Handle()
method is a Console.WriteLine()
that displays a simple message on the console.
Asynchronous Brighter Command Handler
Brighter also provides a convenient way to implement asynchronous command handlers. This is useful if our command handler contains an async operation, such as communicating with an external service. Again, we must start with a command:
public class PingAsyncCommand() : Command(Guid.NewGuid());
We create asynchronous commands by extending from Command
class. There is no difference between synchronous and asynchronous commands. However, there is a difference between asynchronous and synchronous command processors. Let’s create an asynchronous command processor:
public class PingAsyncCommandHandler : RequestHandlerAsync<PingAsyncCommand> { public override async Task<PingAsyncCommand> HandleAsync( PingAsyncCommand command, CancellationToken cancellationToken = default) { await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); Console.WriteLine("[PingAsyncCommandHandler] >> Pong Async!"); return await base.HandleAsync(command, cancellationToken); } }
Similarly, to implement an asynchronous command processor, we extend our command handler class from the RequestHandlerAsync
class and implement the HandleAsync()
method. Inside the HandleAsync()
method, we await for Task.Delay()
, this is to keep things simple throughout this article; in a real-world application, this could be a call to external service, I/O operation, or other asynchronous operation.
Out-Of-Process Event Processing With Brighter in .NET
In addition to in-process command processing and the ability to implement CQRS patterns, Brighter also allows us to process commands out of the process. This is quite useful in many scenarios, such as communication between micro-service, processing demanding tasks independently, processing tasks in a distributed manner, event-driven architecture, and more.
In this section, we will look at how we can build an out-of-process command processor and use RabbitMQ as a message broker. Therefore, let’s start by setting up RabbitMQ.
RabbitMQ Setup
RabbitMQ is a widely adopted, open-source message broker. There are different ways to set up RabbitMQ, but for simplicity, we’ll use Docker:
docker run -it --rm --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3-management
After successfully running the command, RabbitMQ is ready, and we can check by visiting http://localhost:15672
in the browser.
To be able to use RabbitMQ as our event bus in Brighter, we need to install a NuGet package:
dotnet add package Paramore.Brighter.MessagingGateway.RMQ
Next, we will create two services. One service will produce the events, we will call it producer service, and the other will consume the events, which we will call a consumer service.
Brighter Producer
A Producer is a service that is responsible for publishing events.
In Brighter, events indicate that something has happened in our system. Unlike commands, events can have more than one processor.
Let’s create a simple event:
public class PingEvent() : Event(Guid.NewGuid()) { public string Content { get; set; } = $"Ping at {DateTime.Now}"; }
We create a Brighter event, where we simply extend from the Event
class.
Next, we can configure Brighter and add it to the services:
var rmqConnection = new RmqMessagingGatewayConnection { AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672")), Exchange = new Exchange("ping.exchange"), }; var rmqProducerRegistry = new RmqProducerRegistryFactory( rmqConnection, [new() { Topic = new RoutingKey("ping.event") }] ).Create(); builder .Services.AddBrighter() .UseExternalBus(rmqProducerRegistry) .MapperRegistryFromAssemblies(typeof(PingEvent).Assembly);
We start by defining the RabbitMQ configuration and then add Brighter to Services
. We inform Brighter to use RabbitMQ by calling the UseExternalBus()
method and passing the RabbitMQ configuration.
Finally, let’s create an endpoint for producing events:
app.MapGet("/produce", (IAmACommandProcessor commandProcessor) => { commandProcessor.Post(new PingEvent()); return Results.Ok(); } ) .WithOpenApi();
Our /produce
endpoint takes the commandProcessor
object and then posts a PingEvent
by calling the Post()
method. That’s it; we have a producer for publishing events.
The next step is to implement our consumer.
Brighter Consumer
The consumer is a service that listens to and processes events.
Let’s start by implementing a handler for PingEvent
:
public class PingEventHandler : RequestHandler<PingEvent> { public override PingEvent Handle(PingEvent pingEvent) { Console.WriteLine($"[CONSUMER] >> {pingEvent.Id} - {pingEvent.Content}"); return base.Handle(pingEvent); } }
The Handle()
method inside PingEventHandler
is executed when the consumer captures PingEvent
. For simplicity, all we do inside the Handle()
method is to print out to the console the PingEvent
object details. In a real use case, we implement something more practical. Moreover, similar to asynchronous command processors, it is possible to have asynchronous event handlers.
To configure consumers in our dependency injection and leverage consumer capabilities in our project, we need to install two additional NuGet packages:
dotnet add package Paramore.Brighter.ServiceActivator.Extensions.DependencyInjection dotnet add package Paramore.Brighter.ServiceActivator.Extensions.Hosting
Finally, let’s configure our consumer:
var subscriptions = new Subscription[] { new RmqSubscription<PingEvent>( new SubscriptionName("ping.consumer"), new ChannelName("ping.event"), new RoutingKey("ping.event"), isDurable: true, highAvailability: true ), }; var rmqConnection = new RmqMessagingGatewayConnection { AmpqUri = new AmqpUriSpecification(new Uri("amqp://guest:guest@localhost:5672")), Exchange = new Exchange("ping.exchange") }; var rmqMessageConsumerFactory = new RmqMessageConsumerFactory(rmqConnection); builder .Services.AddServiceActivator(options => { options.Subscriptions = subscriptions; options.ChannelFactory = new ChannelFactory(rmqMessageConsumerFactory); }) .AutoFromAssemblies(); builder.Services.AddHostedService<ServiceActivatorHostedService>();
We start by defining the subscription and RabbitMQ configurations. Then, using the AddServiceActivator()
method, we add the service activator. The service activator listens for a message from middleware, which it then delivers to the corresponding handler. Finally, we add the service activator as a hosted service so that it runs in the background.
That’s it! We have our components (RabbitMQ, producer, and consumer) ready, and if we make an HTTP request to the /produce
endpoint, the consumer will be able to capture the events. Let’s send a simple cURL
request to the /produce
endpoint:
curl -X 'GET' 'https://localhost:7101/produce' -H 'accept: */*' --insecure
Next, let’s inspect the logs from the producer and consumer:
Here, on the left, we see a message indicating that our producer has published the event successfully. Subsequently, we see a log indicating the event is received in our consumer logs, by our PingEventHandler
.
Advantages of Brighter in .NET
The .NET ecosystem has several libraries for in-process messaging and implementing the CQRS pattern; MediatR is a popular and good example. Moreover, there are several libraries for working with out-of-process messaging, such as Confluent Kafka client, RabbitMQ .NET client, and more.
However, there is no smooth way to work with different libraries. For example, if we want to migrate from in-process messaging to out-of-process messaging, we have to rewrite a huge chunk of our code. Similarly, changing the underlying technology requires a lot of work for out-of-process message processing.
Brighter makes switching from an in-process approach to out-of-process message processing easy because there is very little we have to change. Also, it provides an abstraction over our underlying technology for out-of-process message processing, making switching from one message broker to another very easy.
For example, if we want to switch from RabbitMQ to Kafka and vice versa, we only need to update the event bus configuration, and there is little to no change we need to make to the event dispatcher and event consumer.
Conclusion
In this article, we learned what Brighter is and have gone over how we can use it for in-process and out-of-process message processing. We learned what Brighter commands and events are. Moreover, we looked at how we can use Brighter with RabbitMQ. Brighter is a feature-rich and lightweight library that makes it easy to switch between different approaches and technologies, as seen in the advantages section.