Getting Started With NServiceBus in .NET: A Beginner's Guide
Learn NServiceBus in .NET from scratch: endpoints, commands, events, handlers, retries, and pub-sub. Simple words, real-life examples, code, and diagrams.
A post office for your code
Think about how a big railway station in India works. Thousands of people need to get messages and parcels to each other. Nobody walks up to a stranger and shouts their message across the platform. Instead, there is a parcel counter. You hand your parcel to the counter, with a clear address on it. The staff make sure it reaches the right place. If the train is late, the parcel waits safely. If one delivery fails, they try again.
You do not need to know which train it goes on. You do not need to wait at the counter. You drop the parcel and walk away, trusting the system.
NServiceBus is the parcel counter for your .NET code. Different parts of your application hand messages to it, with a clear address. NServiceBus makes sure each message reaches the right handler. If a service is busy or down, the message waits in a queue. If processing fails, it tries again. You write the simple part. NServiceBus does the hard, reliable part.
Let us learn how it works, step by step, in plain words.
Why not just call the method directly?
Imagine a small online shop built in the usual way. When an order comes in, your code might do this:
public async Task PlaceOrder(Order order)
{
await _orderRepository.SaveAsync(order);
await _emailService.SendConfirmationAsync(order); // calls another service directly
await _warehouseService.ReserveStockAsync(order); // and another one
await _loyaltyService.AddPointsAsync(order); // and another one
}This looks fine on a calm day. But think about the bad days:
- If the email service is slow, the customer waits for the whole thing.
- If the warehouse service is down, the whole order fails, even though the order was already saved.
- If you add a fifth service later, you must edit this method again.
Everything is glued together tightly. One slow or broken part hurts everyone. This is called tight coupling, and it makes systems fragile.
Messaging fixes this. Instead of calling each service directly, your code drops a message into a queue and moves on. Each service picks up its own message when it is ready. If one is slow, the others are not affected. This is loose coupling, and it is what NServiceBus gives you.
The main ideas, in plain words
NServiceBus has a small set of words. Once you know these five, the rest is easy.
| Word | What it means | Everyday picture |
|---|---|---|
| Message | A small piece of data you send | A parcel |
| Endpoint | A running program that sends or receives messages | A post office branch |
| Handler | A class that runs when a message arrives | The clerk who opens the parcel |
| Transport | The queue technology that carries messages | The trains and trucks |
| Persistence | Where NServiceBus stores its bookkeeping | The filing cabinet |
An endpoint is the heart of it. It is just a normal .NET process, like a console app or a web app, that has NServiceBus switched on inside it. An endpoint can send messages, receive messages, or both. When the endpoint starts, NServiceBus scans your code, finds all your handler classes, and wires them up automatically. You never register handlers by hand.
Commands and events: the two kinds of messages
This is the most important idea, so let us go slow. There are two kinds of messages, and they feel very different.
A command is an instruction. It says, "please do this." Like PlaceOrder or ChargeCard. A command is bossy. It points at one service and expects it to act. A command has exactly one handler. You use Send to deliver a command.
An event is an announcement. It says, "this already happened." Like OrderPlaced or PaymentReceived. Notice the past tense. An event is polite. It does not tell anyone what to do. It just shares news. Many services can listen to the same event, or none at all. You use Publish to share an event.
Here is a simple way to remember it: a command is like sending one friend a WhatsApp message asking them to buy milk. An event is like posting "I just reached home!" in a group, where anyone who cares can react.
| Command | Event | |
|---|---|---|
| Meaning | "Do this" | "This happened" |
| Tense | Present (PlaceOrder) | Past (OrderPlaced) |
| Handlers | Exactly one | Zero or many |
| API used | Send | Publish |
| Who decides the receiver | The sender | The subscribers |
How a command flows
Steps
Sender
Calls Send(PlaceOrder)
Queue
Command waits safely
Handler
One handler runs
Done
Order created
Setting up your first endpoint
Let us build a tiny endpoint. We will use the Learning Transport, which stores messages as files on your disk. It needs no install and is perfect for practice. Later you swap it for RabbitMQ or Azure Service Bus with one line.
First, create a console app and add the package:
// In a terminal:
// dotnet new console -n Shipping
// dotnet add package NServiceBus
using NServiceBus;
var builder = Host.CreateApplicationBuilder(args);
// Give the endpoint a name. This becomes its queue name.
var endpointConfig = new EndpointConfiguration("Shipping");
// Use the Learning Transport for practice (files on disk).
endpointConfig.UseTransport(new LearningTransport());
// Tell NServiceBus to host inside the .NET generic host.
builder.UseNServiceBus(endpointConfig);
var app = builder.Build();
await app.RunAsync();That is a complete endpoint. When it starts, NServiceBus creates a queue named Shipping, scans for handlers, and waits for messages. The endpoint name matters: it is the address other endpoints use to send to you.
Defining a message
A message is just a plain C# class. It carries data, nothing more. By convention you mark it so NServiceBus knows what kind it is.
// A command: an instruction for one service.
public class PlaceOrder : ICommand
{
public string OrderId { get; set; } = string.Empty;
public string ProductCode { get; set; } = string.Empty;
public int Quantity { get; set; }
}
// An event: news that something happened (past tense).
public class OrderPlaced : IEvent
{
public string OrderId { get; set; } = string.Empty;
}Using ICommand and IEvent is the friendly way. It makes your intent obvious and lets NServiceBus check that you are using Send for commands and Publish for events. You can also use conventions or attributes, but the marker interfaces are the clearest place to begin.
Keep messages simple and small. They get serialized (turned into bytes) to travel through the queue, so use plain properties. Do not put logic, database connections, or huge objects inside a message.
Writing a handler
A handler is the clerk who opens the parcel. It is a class that implements IHandleMessages<T>, where T is the message type.
public class PlaceOrderHandler : IHandleMessages<PlaceOrder>
{
private readonly ILogger<PlaceOrderHandler> _logger;
public PlaceOrderHandler(ILogger<PlaceOrderHandler> logger)
{
_logger = logger;
}
public async Task Handle(PlaceOrder message, IMessageHandlerContext context)
{
_logger.LogInformation("Placing order {OrderId} for {Qty} x {Product}",
message.OrderId, message.Quantity, message.ProductCode);
// ... save the order in your database here ...
// Now announce to the world that the order was placed.
await context.Publish(new OrderPlaced { OrderId = message.OrderId });
}
}Notice two things. First, the handler asks for ILogger in its constructor. NServiceBus uses the normal .NET dependency injection, so anything you registered is available here. Second, the IMessageHandlerContext is your toolbox. From it you can Send more commands, Publish events, or Reply to whoever sent the message. Here we publish OrderPlaced so other services can react.
You never write code to find or call this handler. NServiceBus discovers it at startup and runs it whenever a PlaceOrder message arrives.
Sending a command
To send a command, you ask the message session to Send it. In a web app you usually inject IMessageSession.
app.MapPost("/orders", async (PlaceOrder command, IMessageSession messageSession) =>
{
// Hand the command to NServiceBus and return immediately.
await messageSession.Send(command);
return Results.Accepted();
});The web request finishes fast because it does not wait for the order to be fully processed. It just drops the parcel at the counter. The Shipping endpoint will pick it up and run the handler in the background. If the Shipping endpoint is busy or restarting, the command waits safely in the queue until it is ready.
From web request to background work
Steps
HTTP POST
User places order
Send
Command queued, 202 returned
Queue
Waits for Shipping
Handler
Order processed
Publish event
OrderPlaced shared
Publish and subscribe: telling many at once
Now the fun part. When the order handler publishes OrderPlaced, who hears it?
Anyone who subscribed. A subscriber is just another endpoint with a handler for OrderPlaced. For example, an Email endpoint and a Loyalty endpoint can both handle the same event, each doing its own job.
// In the Email endpoint
public class SendConfirmationHandler : IHandleMessages<OrderPlaced>
{
public Task Handle(OrderPlaced message, IMessageHandlerContext context)
{
// send a "thank you for your order" email
return Task.CompletedTask;
}
}
// In the Loyalty endpoint
public class AddPointsHandler : IHandleMessages<OrderPlaced>
{
public Task Handle(OrderPlaced message, IMessageHandlerContext context)
{
// add reward points to the customer
return Task.CompletedTask;
}
}The publisher does not know these endpoints exist. It just shouts "OrderPlaced!" into the air. Each subscriber catches its own copy. This is the magic of publish-subscribe: you can add a tenth listener next year without touching the order code at all.
By default NServiceBus uses auto-subscribe. When a subscriber endpoint starts, it looks at the events it has handlers for and subscribes to them on its own. You do not manage subscription lists by hand.
When things go wrong: automatic retries
Real systems fail. A database may be locked for a moment. A network may blink. NServiceBus is built for this and retries for you, in two stages.
First come immediate retries. The message is tried again right away, a few times. This handles tiny, momentary glitches like a brief lock.
If the message still fails, delayed retries kick in. NServiceBus waits a short while, then tries again, waiting a little longer each round. This gives a struggling database time to recover.
If every retry fails, the message is moved to a separate error queue. This is very important: the bad message steps aside so it does not block all the good messages behind it. Later, once you fix the bug, you can replay the failed messages from the error queue, and they get processed as if nothing happened.
| Stage | When it runs | What it is good for |
|---|---|---|
| Immediate retries | Right away, a few times | Tiny glitches, brief locks |
| Delayed retries | After a growing wait | Database recovering, service restarting |
| Error queue | After all retries fail | Real bugs you must fix, then replay |
You configure these retries in a few lines, and the defaults are sensible:
var recoverability = endpointConfig.Recoverability();
recoverability.Immediate(immediate => immediate.NumberOfRetries(3));
recoverability.Delayed(delayed =>
{
delayed.NumberOfRetries(2);
delayed.TimeIncrease(TimeSpan.FromSeconds(10));
});Choosing a transport
The transport is the queue technology under the hood. NServiceBus does not lock you into one. You write the same handlers and the same Send/Publish calls, and only the configuration line changes.
| Transport | Good for | Notes |
|---|---|---|
| Learning Transport | Practice and demos | Files on disk, no install, not for production |
| RabbitMQ | Self-hosted, on-prem or cloud | Popular, supports native pub-sub |
| Azure Service Bus | Apps on Azure | Fully managed by Microsoft |
| Amazon SQS / SNS | Apps on AWS | Managed queues and topics |
Swapping is as small as this:
// From this (practice):
endpointConfig.UseTransport(new LearningTransport());
// To this (Azure Service Bus in production):
endpointConfig.UseTransport(new AzureServiceBusTransport(connectionString));Because your handlers never mention the transport, you can start on the Learning Transport today and move to a real broker tomorrow with almost no code change. This is one of the biggest gifts NServiceBus gives a team.
A note on licensing and the .NET ecosystem
You may have heard that some popular .NET libraries changed their pricing. As of 2026, both MediatR and MassTransit moved to commercial licensing for newer versions. NServiceBus has always been a commercial product from Particular Software, but it offers a free Community Edition for small teams and non-production use, plus the free Learning Transport for practice. Before you ship NServiceBus in a paid product, read the current licensing page from Particular, because terms can change. For learning and small projects, you can get started at no cost.
This matters when you compare tools. NServiceBus is not just a message router. It bundles retries, pub-sub, long-running workflows (called sagas), and the outbox for safe message delivery, all in one well-supported package. For many teams, that maturity is worth the license.
A simple end-to-end picture
Let us put the whole flow together with three endpoints: a Web endpoint that takes orders, a Shipping endpoint that processes them, and Email plus Loyalty endpoints that react to the news.
Read it from the top. The user posts an order. The Web endpoint sends a PlaceOrder command into the Shipping queue and answers the user right away. The Shipping handler processes the order, then publishes an OrderPlaced event. The Email and Loyalty endpoints each catch that event and do their own work. No endpoint waits on another. Each one is calm and independent.
Good habits to start with
A few simple rules will keep your NServiceBus app healthy from day one.
- Name events in the past tense.
OrderPlaced, notPlaceOrderEvent. It keeps the command-versus-event line clear. - Keep messages small. They travel through a queue. Send IDs and a few fields, not whole object graphs.
- One responsibility per handler. A handler should do one clear job. If it grows, split it.
- Make handlers idempotent. Because retries exist, a handler might run twice for the same message. Design it so running twice does no harm, for example by checking "did I already process this order ID?"
- Turn on the Outbox when a handler both writes to a database and sends messages. It makes the two happen together safely, so you never save data without sending the message, or send a message without saving the data.
That last point connects to a wider pattern. NServiceBus has a built-in outbox feature that solves the dual-write problem for you, which is a common source of subtle bugs in messaging systems.
Quick recap
- NServiceBus is a parcel counter for your .NET code. Parts of your app hand it messages, and it delivers them safely and reliably.
- An endpoint is a running program with NServiceBus inside. It can send, receive, or both, and it finds your handlers automatically at startup.
- A command says "do this" and has one handler; you use Send. An event says "this happened" (past tense) and can have many listeners; you use Publish.
- A handler is a class implementing
IHandleMessages<T>. TheIMessageHandlerContextlets you send, publish, and reply. - Publish-subscribe lets you add new listeners without changing the publisher. Auto-subscribe wires it up for you.
- When work fails, NServiceBus does immediate retries, then delayed retries, then moves the message to the error queue so it can be fixed and replayed.
- The transport (Learning, RabbitMQ, Azure Service Bus, SQS) is just one config line. Your handlers never change when you swap it.
- Start free with the Learning Transport and Community Edition, and check Particular's current licensing before shipping in a paid product.
References and further reading
- NServiceBus Step-by-step Tutorial — Particular Docs
- Getting started lesson — Particular Docs
- Sending a command — Particular Docs
- Publishing events — Particular Docs
- Messages, events, and commands — Particular Docs
- Publish-Subscribe — Particular Docs
- Handlers — Particular Docs
- Build message-driven apps with NServiceBus and Azure Service Bus — Microsoft Learn
- Getting Started With NServiceBus in .NET — Milan Jovanović
Related Patterns
Event-Driven Microservices with Azure Service Bus in .NET
A friendly, step-by-step guide to building event-driven microservices in .NET using Azure Service Bus topics, subscriptions, and the ServiceBusProcessor.
Event-Driven Architecture in .NET with RabbitMQ: A Beginner's Guide
Learn event-driven architecture in .NET with RabbitMQ using simple words, real-life examples, exchanges, queues, and clean async C# code you can copy.
The Outbox Pattern in .NET: Never Lose a Message Again
Learn the Outbox Pattern in .NET with simple, real-life examples. Save your data and your messages in one transaction so a broker outage can never lose an event. Includes EF Core code, diagrams, and best practices.
Implementing the Saga Pattern With Wolverine in .NET
Learn the saga pattern in .NET with Wolverine: stateful sagas, Start and Handle methods, timeouts, and compensation. Simple words, examples, and diagrams.
Synchronous vs Asynchronous Communication in Microservices (.NET Guide)
A simple, friendly guide to synchronous vs asynchronous communication in microservices, with .NET examples, diagrams, tables, and clear rules on when to use each.
Understanding Microservices: Core Concepts and Benefits for .NET
A beginner-friendly guide to microservices in .NET: what they are, the core ideas behind them, their real benefits and trade-offs, and when to use them.