Iron Software amplifies its toolkit, introducing a suite of 9 products, encompassing PDF, OCR (see case study), barcode, spreadsheet, and more. This expansion enriches the .NET landscape, offering developers a diverse set of tools for enhanced productivity in application development.
ABP Framework is a platform for building enterprise-grade ASP.NET Core applications. It comes with production-ready components, modular architecture, and Domain-Driven Design. And exciting news: Call for Papers (CFP) submission for the ABP Dotnet Conference 2024 on May 8-9 is now open.
Suppose you're building a modular monolith, a type of software architecture where different components are organized into loosely coupled modules. Or you might need to process data asynchronously. You'll need a tool or service that allows you to implement this.
Messaging plays a crucial role in modern software architecture, enabling communication and coordination between loosely coupled components.
An in-memory message bus is particularly useful when high performance and low latency are critical requirements.
In today's issue, we will:
Create the required messaging abstractions
Build an in-memory message bus using channels
Implement an integration event processor background job
Demonstrate how to publish and consume messages asynchronously
Let's dive in.
When To Use an In-Memory Message Bus
I have to preface this by saying that an in-memory message bus is far from a silver bullet. There are many caveats to using it, as you will soon learn.
But first, let's start with the pros of using an in-memory message bus:
Because it works in memory, you have a very low-latency messaging system
You can implement asynchronous (non-blocking) communication between components
However, there are a few drawbacks to this approach:
Potential for losing messages if the application process goes down
It only works inside of a single process, so it's not useful in distributed systems
A practical use case for an in-memory message bus is when building a modular monolith. You can implement communication between modules using integration events. When you need to extract some modules into a separate service, you can replace the in-memory bus with a distributed one.
Defining The Messaging Abstractions
We will need a few abstractions to build our simple messaging system. From the client's perspective, we really only need two things. One abstraction is to publish messages, and another is to define a message handler.
The IEventBus interface exposes the PublishAsync method. This is what we will use to publish messages. There's also a generic constraint defined that only allows passing in an IIntegrationEvent instance.
I want to be practical with the IIntegrationEvent abstraction, so I'll use MediatR for the pub-sub support. The IIntegrationEvent interface will inherit from INotification. This allows us to easily define IIntegrationEvent handlers using INotificationHandler<T>. Also, the IIntegrationEvent has an identifier, so we can track its execution.
The abstract IntegrationEvent serves as a base class for concrete implementations.
usingMediatR;publicinterfaceIIntegrationEvent:INotification{Guid Id {get;init;}}publicabstractrecordIntegrationEvent(Guid Id):IIntegrationEvent;
Simple In-Memory Queue Using Channels
The System.Threading.Channels namespace provides data structures for asynchronously passing messages between producers and consumers. Channels implement the producer/consumer pattern. Producers asynchronously produce data, and consumers asynchronously consume that data. It's an essential pattern for building loosely coupled systems.
One of the primary motivations behind the adoption of .NET Channels is their exceptional performance characteristics. Unlike traditional message queues, Channels operate entirely in memory. This has the disadvantage of the potential for message loss if the application crashes.
The InMemoryMessageQueue creates an unbounded channel using the Channel.CreateUnbounded bounded. This means the channel can have any number of readers and writers. It also exposes a ChannelReader and ChannelWriter, which allow consumers to publish and consume messages.
The IEventBus implementation is now straightforward with the use of channels. The EventBus class uses the InMemoryMessageQueue to access the ChannelWriter and write an event to the channel.
With the EventBus implementing the producer, we need a way to consume the published IIntegrationEvent. We can implement a simple background service using the built-in IHostedService abstraction.
The IntegrationEventProcessorJob depends on the InMemoryMessageQueue, but this time for reading (consuming) messages. We'll use the ChannelReader.ReadAllAsync method to get back an IAsyncEnumerable. This allows us to consume all the messages in the Channel asynchronously.
The IPublisher from MediatR helps us connect the IIntegrationEvent with the respective handlers. It's important to resolve it from a custom scope if you want to inject scoped services into the event handlers.
With all of the necessary abstractions in place, we can finally use the in-memory message bus.
The IEventBus service will write the message to the Channel and immediately return. This allows you to publish messages in a non-blocking way, which can improve performance.
internalsealedclassRegisterUserCommandHandler(IUserRepository userRepository,IEventBus eventBus): ICommandHandler<RegisterUserCommand>{publicasyncTask<User>Handle(RegisterUserCommand command,CancellationToken cancellationToken){// First, register the user.User user =CreateFromCommand(command); userRepository.Insert(user);// Now we can publish the event.await eventBus.PublishAsync(newUserRegisteredIntegrationEvent(user.Id), cancellationToken);return user;}}
This solves the producer side, but we also need to create a consumer for the UserRegisteredIntegrationEvent message. This part is greatly simplified because I'm using MediatR in this implementation.
We need to define an INotificationHandler implementation handling the integration event UserRegisteredIntegrationEvent. This will be the UserRegisteredIntegrationEventHandler.
When the background job reads the UserRegisteredIntegrationEvent from the Channel, it will publish the message and execute the handler.
internalsealedclassUserRegisteredIntegrationEventHandler:INotificationHandler<UserRegisteredIntegrationEvent>{publicasyncTaskHandle( UserRegisteredIntegrationEvent event,CancellationToken cancellationToken){// Asynchronously handle the event.}}
Improvement Points
While our basic in-memory message bus is functional, there are several areas we can improve:
Resilience - We can introduce retries when we run into exceptions, which will improve the reliability of the message bus.
Idempotency - Ask yourself if you want to handle the same message twice. The idempotent consumer pattern elegantly solves this problem.
Dead Letter Queue - Sometimes, we won't be able to handle a message correctly. It's a good idea to introduce a persistent storage for these messages. This is called a Dead Letter Queue, and it allows for troubleshooting at a later time.
We've covered the key aspects of building an in-memory message bus using .NET Channels. You can extend this further by implementing the improvements for a more robust solution.
Remember that this implementation only works inside of one process. Consider using a real message broker if you need a more reliable solution.
P.S. 2 ways I can help you accelerate your skills:
Pragmatic Clean Architecture
The complete blueprint for building production-ready applications using Clean Architecture. Join 2,400+ other students to accelerate your growth as a software architect.
Automatically Register Minimal APIs in ASP.NET Core Read on: milanjovanovic.tech Read time: 4 minutes The .NET Weekly is brought to you by: Become a Postman master with Postman Intergalactic
Using Scoped Services From Singletons in ASP.NET Core Read on: milanjovanovic.tech Read time: 3 minutes The .NET Weekly is brought to you by: Become a Postman master with Postman
Getting the Current User in Clean Architecture Read on: milanjovanovic.tech Read time: 4 minutes The .NET Weekly is brought to you by: Blending AI with UI: Crafting LLM UIs in Blazor,
How I Made My EF Core Query 3.42x Faster With Batching Read on: milanjovanovic.tech Read time: 4 minutes The .NET Weekly is brought to you by: How do you choose the right PDF library?
ISSUE #434 24th of November 2024 Hi Kotliners! Next week is the last one to send a paper proposal for the KotlinConf. We hope to see you there next year. Announcements State of Kotlin Scripting 2024
More Time to Write A fully functional clock that ticks backwards, giving you more time to write. Tech Stuff Martijn Faassen (FWIW I don't know how to use any debugger other than console.log) People
Also: Best Outdoor Smart Plugs, and More! How-To Geek Logo November 23, 2024 Did You Know After the "flair" that servers wore—buttons and other adornments—was made the butt of a joke in the
JSK Daily for Nov 23, 2024 View this email in your browser A community curated daily e-mail of JavaScript news React E-Commerce App for Digital Products: Part 4 (Creating the Home Page) This component
What (and who) video-based social media leaves out. Here's a version for your browser. Hunting for the end of the long tail • November 23, 2024 Not Ready For The Camera Why hasn't video
Daily Coding Problem Good morning! Here's your coding interview problem for today. This problem was asked by Microsoft. You are given an string representing the initial conditions of some dominoes.
These two maps compare the world's tallest countries, and the world's shortest countries, by average height. View Online | Subscribe | Download Our App TIME IS RUNNING OUT There's just 3
November 23, 2024 | Read Online Subscribe | Advertise Good Morning. Welcome to this special edition of The Deep View, brought to you in collaboration with Convergence. Imagine if you had a digital