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.

To download the source code for this article, you can visit our GitHub repository.

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.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!
If you prefer a more detailed explanation, check out our articles CQRS and MediatR in ASP.NET Core and CQRS Validation Pipeline with MediatR and FluentValidation.

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.

To learn more about event-driven architecture, please check our article Event-Driven Architecture in C#.

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:

Showing Logs of Brighter Consumer And Producer

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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!