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?
Top Tech Content sent at Noon! Boost Your Article on HackerNoon for $159.99! Read this email in your browser How are you, @newsletterest1? 🪐 What's happening in tech today, January 15, 2025? The
Free access to this go-to-guide for invaluable insights and practical advice to secure your software supply chain. The Hacker News Software Supply Chain Security for Dummies There is no longer doubt
✨ Better Pixel photos; How to quit Meta; The next TikTok? -- ZDNET ZDNET Tech Today - US January 15, 2025 ai-prompting-mistakes The five biggest mistakes people make when prompting an AI Ready to
Plus generating random art, sending emails, and a variety of gopher images you can use. | #538 — January 15, 2025 Unsub | Web Version Together with Posthog Go Weekly An Interactive Tour of Go 1.24 — A
Masculine Startups • The Fall of Xbox • Meta's Misinformation Off Switch • TikTok's Switch Off The Spyglass Dispatch is a newsletter sent on weekdays featuring links and commentary on timely
New blogs from Syncfusion Introducing the New .NET MAUI Bottom Sheet Control By Naveenkumar Sanjeevirayan This blog explains the features of the Bottom Sheet control introduced in the Syncfusion .NET
THN Daily Updates Newsletter cover The Kubernetes Book: Navigate the world of Kubernetes with expertise , Second Edition ($39.99 Value) FREE for a Limited Time Containers transformed how we package and