Skip to main content

Messaging Made Easy - Practical Demos with NATS and .NET

7 min read

I just discovered that NATS has released a new NuGet package. The previous NATS.Client has been iterated and replaced by NATS.NET. Out of curiosity, I decided to take a closer look and revisit NATS along the way.

info

NATS.Client has not been deprecated; it mainly supports older versions of .NET development environments. Back in 2023, I created a demo project using NATS.Client as a learning resource. If you're interested, you can check out the project via this link.

Here’s the link to the code used for demonstration.

What is NATS

NATS is a messaging system, often referred to as a message broker or messaging agent.

As always, the official team’s definition of the tool is more accurate than mine. Below is the explanation of NATS provided by the official team.

NATS is a lightweight and high-performance messaging system designed for asynchronous communication among different software components, with modern clustering, security and persistence streaming support out of the box, in a single compact binary with no dependencies, available for any modern platform, enabling a vast variety of deployment options from edge, IoT, Kubernetes to bare-metal.

NATS Concepts

NATS is a subject-based messaging system. If you’ve worked with publish-subscribe messaging systems or services before, getting started with NATS should feel quite intuitive.

pub-sub

To illustrate, let’s consider the diagram above. A NATS environment requires at least three components: a publisher (producer), a NATS server, and a subscriber (consumer).

  • The subscriber usually represents the client side, while the publisher represents the producer.
  • If the client wants to receive messages from the producer, it must first subscribe to the producer's subject to establish a listening channel.

What subject mean?

Think of a subject as the anchor point for communication. The ratio between subscribers and publishers is often 1:N or N:N. If a subscriber wants to receive the correct messages, it needs to rely on subject to ensure it’s listening to the right channel. Conversely, a subscriber can unsubscribe from a subject at any time to stop receiving messages related to it.

I hope this explanation simplifies the concept of NATS. If you’d like to learn more or have any questions, I recommend checking out the official documentation for a deeper understanding.

What are we going to build?

Next, we will simulate a demo scenario for NATS.

We will create two objects: receiver (subscriber) and sender (publisher), and then explore and experiment with some of NATS’s core features step by step.

In a .NET environment, we will experience NATS's five core functionalities:

  • Pub-Sub
  • Wildcards
  • Queue Group (Load Balancing)
  • Request-Response
  • Streaming

The demo project code can be found above, and you can download it directly.

Prerequisites: Initializing the NATS Server and Project

Before getting started, you need to download the latest version of the NATS Server. It’s extremely lightweight, around 10MB, and won’t take up much space on your computer.

Steps to Set Up NATS Server:

  1. Visit the official GitHub repository to find the latest release.
  2. Choose the version that matches your OS and download it.
  3. After downloading, extract the file to any directory you prefer.

The NATS Server is an executable program with an .exe extension. Simply double-click to start it.

Important Note:

For the demo project, the NATS Server must remain online throughout the process. Otherwise, the programs in the demonstration will encounter errors.

Optionally, you can attach a .conf or .json file to configure your NATS Server. However, this demo focuses on simplicity and will not cover advanced configurations. If you’re interested, refer to this documentation.

Setting Up Subscriber and Publisher:

You’ll need to create two separate console applications: one for the subscriber and one for the publisher. Use the following commands to create them quickly:

The Receiver

BASH
mkdir Nats.Receiver
cd Nats.Receiver
dotnet new console
dotnet add package NATS.Net

The Sender

BASH
mkdir Nats.Sender
cd Nats.Sender
dotnet new console
dotnet add package NATS.Net

Core NATS

Now, we’re finally diving into the main content. This section will be divided into two parts: Core Features and JetStream Features.

Let’s start with the core functionalities of NATS. Simply put, these are the foundational features of NATS that revolve around subject-based publish/subscribe messaging.

Without further ado, let’s get started!

Pub/Sub

Let’s begin with the Sender. This Sender will send messages to all subscribers who are subscribed to the subject nats.demo.pubsub, as illustrated below:

ps_flow

Nats.Sender/Program.cs
await using var nc = new NatsClient();

await nc.PublishAsync("nats.demo.pubsub", "Message From Producer");

Here, the Subscriber will subscribe to the "nats.demo.pubsub" subject. Whenever any messages related to this subject are published by the Publisher, the Subscriber will receive those messages.

Nats.Receiver/Program.cs
await using var nc = new NatsClient();

//Loop unstop to keep watch on subject
await foreach (var msg in nc.SubscribeAsync<string>(subject: "nats.demo.pubsub"))
{
Console.WriteLine($"Received: {msg.Data}");
}

See, it’s that simple!

You can define the types of messages that are returned, for example:

CSHARP
public record Line(int Id, string Name);

//Sender
await nc.PublishAsync<Line>("nats.demo.pubsub", new Line(1,"line_1"));

//Receiver
await foreach (var msg in nc.SubscribeAsync<Line>(subject: "nats.demo.pubsub"))
{
Console.WriteLine($"Received: id: {msg.Data.Id},name:{msg.Data.Name}");
}

You can run multiple Subscribers simultaneously to see how they all receive the same messages from the Publisher. This is one of the core features of the Pub-Sub model in NATS: when a message is published to a subject, all subscribers listening to that subject receive the same message independently.

ps-1

Wildcard

NATS provides two types of wildcards that enable hierarchical subject structures. The hierarchical approach improves the efficiency of message delivery and subject usage.

The Wildcards in NATS

1.* (Single-Level Wildcard)

This wildcard matches any single level in the subject hierarchy.

Example: If you subscribe to nats.demo.*, the following subjects will match:

  • nats.demo.pubsub1
  • nats.demo.pubsub2

Subject that won’t match:

  • nats.demo.pubsub1.xxx
  • nats.demo.pubsub2.xxx

If the Publisher publishes a message to nats.demo.*, subscribers who are subscribed to either nats.demo.pubsub1 or nats.demo.pubsub2 will receive the message.

2.> (Multi-Level Wildcard)

The > wildcard matches any number of levels in the subject hierarchy.

Example: If you subscribe to nats.demo.>, the following subjects will match:

  • nats.demo.p1.p11.p111
  • nats.demo.p2.p22

Subject that won’t match:

  • nats.hello.xxxx
  • hello.demo.xxxx

As long as the subscriber subscribes to a subject starting with nats.demo, they will receive messages from any publisher publishing to that hierarchical structure, regardless of how many levels follow.

Summary:

  • The * wildcard allows matching a single level of the subject hierarchy.
  • The > wildcard allows matching multiple levels in the hierarchy.

Queue Groups

NATS provides an additional feature called Queues which allows subscribers to register themselves as part of a queue. Subscribers in the same queue group share the responsibility of processing messages. When messages are published to a subject, they are randomly distributed among the subscribers in the queue group, enabling load balancing for response handling.

Here’s how you can implement a simple load balancing scenario where multiple subscribers (workers) share the workload for a subject:

Nats.Sender/Program.cs
for (int i = 1; i <= 10; i++)
{
string message = $"Message {i}";

Console.WriteLine($"Sending: {message}");

await nc.PublishAsync("nats.demo.queuegroups", message);

Thread.Sleep(100);
}
Nats.Receiver/Program.cs
await foreach (var msg in nc.SubscribeAsync<string>(subject: "nats.demo.queuegroups", queueGroup: "load-balancing-queue"))
{
Console.WriteLine($"QG Received: {msg.Data}");
}

20241219030328710

When the Publisher sends ten messages, due to the use of a Queue Group, the messages are randomly distributed among the subscribers.

As a result, the number of messages each subscriber receives might not be equal. This is expected behavior because Queue Groups ensure that messages are handled by a single subscriber within the group, with messages being "pulled" by subscribers as they are available.

Request-Response

In modern distributed systems, the request-response pattern is a common approach. After sending a request, the application either waits for a response within a certain timeout or asynchronously receives the response.

Here’s how you can set up a Publisher to send a request and a Subscriber to respond:

Nats.Sender/Program.cs

for (int i = 1; i <= 10; i++)
{
string message = $"Message {i}";

NatsMsg<string> reply = await nc.RequestAsync<string,string>("nats.demo.requestresponse", message);

// You can define options for publish or subscribe in the request, for example, you can require the subscriber to respond within five seconds
// NatsMsg<string> reply = await nc.RequestAsync<string,string>("nats.demo.requestresponse", message,replyOpts:new NatsSubOpts
// {
// Timeout=TimeSpan.FromSeconds(5)
// });

var responseMsg = reply.Data;
var currentTime = DateTime.Now.ToString("T");

Console.WriteLine($"Response({currentTime}): {responseMsg}");

Thread.Sleep(100);
}

Subscribe to the Request-Reply pattern subject.

Nats.Receiver/Program.cs
await foreach (var msg in nc.SubscribeAsync<string>(subject: "nats.demo.requestresponse"))
{
Console.WriteLine($"RR Received: {msg.Data}");
var replyMessage = $"Reply {msg.Data}";
Thread.Sleep(1000); //simulating subscriber reply after 1 second
await msg.ReplyAsync(replyMessage);
}

20241219034058389

To simulate the request-reply scenario, I added a 1-second delay to the subscriber's response time, making it easier to observe the request-reply flow. From the image above, we can clearly see that the publisher receives the message from the subscriber one second after publishing the message.

This form of communication is suitable for building systems that require immediate responses or actions that need confirmation, results, or data.

JetStream

JetStream is an extension of NATS that provides advanced message persistence and management capabilities for distributed systems, making it suitable for scenarios that require reliability, replayability, and persistence.

For example, if the subscriber experiences some downtime and is unable to receive messages, JetStream will store the event and record the sent messages. When the subscriber recovers, it can replay the missed messages.

Since data needs to be stored, it will occupy physical space on the hardware. You might be concerned about space usage, but JetStream allows you to define the maximum allowed storage space, making the space usage manageable.

Now that we've introduced it, let's discuss how to start it.

Starting JetStream is simple; you just need to add the -js flag to the nats-server command.

For example, if your nats-server.exe is located at C:/Download/nats-server.exe, you can use the following command to start NATS with JetStream functionality enabled:

BASH
cd C:/Download
nats-server -js

Code Snippets

Nats.Sender/Program.cs
await using var nc = new NatsClient();

var js = nc.CreateJetStreamContext();
var stream_subject = "streams.>";
await js.CreateStreamAsync(new StreamConfig(name: "STREAM_DEMO", subjects: [stream_subject]));

// Simulation snippet
for (int i = 1; i <= 25; i++)
{
string message = $"[{DateTime.Now.ToString("T")}] Message {i}";
Console.WriteLine($"Sending {message}");
string subject = stream_subject + $".DATA.{Guid.NewGuid().ToString()}";
var ack =await js.PublishAsync(subject, message);
ack.EnsureSuccess();
}
Nats.Receiver/Program.cs
await using var nc = new NatsClient();
var js = nc.CreateJetStreamContext();

// Add this to make your messages durable
var durableConfig = new ConsumerConfig
{
Name = "durable_processor",
DurableName = "durable_processor",
};

using var cts = new CancellationTokenSource();

//Give some time for Producer side initialize the stream
Thread.Sleep(2000);

var consumer = await js.CreateOrUpdateConsumerAsync(stream: "STREAM_DEMO", durableConfig);


await foreach (var msg in consumer.ConsumeAsync<string>().WithCancellation(cts.Token))
{
var data = msg.Data;
Console.WriteLine($"Stream: {data}");
await msg.AckAsync();
}

The output is actually not much different from other pub-sub subject events. What makes JetStream special is its persistent message storage and replay capabilities, which are particularly valuable during downtime.

From the code, we can see the syntax for acknowledgment (ack). If the subscriber fails to notify the producer that the message has been received, JetStream will log the event and replay the missed message when the subscriber responds.

Ending

This chapter is limited in scope and cannot fully demonstrate all of NATS' features.

NATS has many other excellent extensions and advanced configurations, and I highly recommend visiting the official website to explore more.

Loading Comments...