Shuttle.Hopper
22.1.0
Prefix Reserved
dotnet add package Shuttle.Hopper --version 22.1.0
NuGet\Install-Package Shuttle.Hopper -Version 22.1.0
<PackageReference Include="Shuttle.Hopper" Version="22.1.0" />
<PackageVersion Include="Shuttle.Hopper" Version="22.1.0" />
<PackageReference Include="Shuttle.Hopper" />
paket add Shuttle.Hopper --version 22.1.0
#r "nuget: Shuttle.Hopper, 22.1.0"
#:package Shuttle.Hopper@22.1.0
#addin nuget:?package=Shuttle.Hopper&version=22.1.0
#tool nuget:?package=Shuttle.Hopper&version=22.1.0
Shuttle.Hopper
Shuttle.Hopper is a comprehensive message bus implementation that facilitates message-driven communication between different components of an application. It provides a robust and flexible architecture for building distributed systems.
Installation
dotnet add package Shuttle.Hopper
Registration
To use Shuttle.Hopper, you need to register it with your service collection:
services.AddHopper(options =>
{
// Configure Hopper options here
});
Note: While configuring options via code is supported as shown above, binding from IConfiguration (e.g., using appsettings.json) is preferable in most cases.
The AddHopper method returns a HopperBuilder that can be used to further configure the bus, such as adding message handlers and subscriptions.
Messaging Operations
The core interface for sending and publishing messages is IBus.
IBus
You can use IBus to send commands or publish events:
// Sending a command
await bus.SendAsync(new MyCommand { Value = "Hello" });
// Publishing an event
await bus.PublishAsync(new MyEvent { OccurredAt = DateTime.Now });
Message Handlers
Shuttle.Hopper supports two types of message handlers:
IContextMessageHandler<T>
This handler receives an IHandlerContext<T> providing access to message metadata and the ability to send or publish messages within the handler context.
public class MyContextHandler : IContextMessageHandler<MyMessage>
{
public async Task HandleAsync(IHandlerContext<MyMessage> context, CancellationToken cancellationToken = default)
{
// Handle the message
var message = context.Message;
// Use the context to send another message
await context.SendAsync(new AnotherMessage());
}
}
IMessageHandler<T>
This handler receives the message directly, which is useful for simpler handling scenarios.
public class MySimpleHandler : IMessageHandler<MyMessage>
{
public async Task HandleAsync(MyMessage message, CancellationToken cancellationToken = default)
{
// Handle the message
Console.WriteLine(message.Value);
}
}
Delegate Handlers
You can also register delegates (such as lambda expressions) directly to handle messages without implementing an interface. A delegate can optionally accept an IHandlerContext<T> or just the message type, and must return a Task or ValueTask. An optional CancellationToken may also be passed into the delegate if required.
services.AddHopper(options => { ... })
.AddMessageHandler(async (IHandlerContext<MyMessage> context, CancellationToken cancellationToken) =>
{
// Handle the message using the context
await context.SendAsync(new AnotherMessage(), builder: null, cancellationToken: cancellationToken);
})
.AddMessageHandler(async (MyMessage message) =>
{
// Handle the message directly
Console.WriteLine(message.Value);
});
Registering Message Handlers
Interface-based message handlers can be registered using the HopperBuilder:
services.AddHopper(options => { ... })
.AddMessageHandler<MyContextHandler>()
.AddMessageHandler<MySimpleHandler>()
.AddMessageHandlersFrom(typeof(MyContextHandler).Assembly);
Subscriptions
You can add subscriptions to the bus using the HopperBuilder:
services.AddHopper(options => { ... })
.AddSubscription<MyEvent>();
Transports
Shuttle.Hopper abstracts over physical transport implementations via the ITransport and ITransportFactory interfaces. To perform actual message passing, you'll need to install an implementation package suited for your infrastructure (e.g., MSMQ, RabbitMQ, Azure Service Bus) and ensure its transport factory is registered. Depending on the transport, you may also define UriMappingOptions to map your application's abstract logical URIs to physical queue locations.
Processing Concepts
Shuttle.Hopper provides advanced architectural features such as inbox processing, outbox atomic messaging, and deferred dispatch.
Inbox and Outbox Processing
The InboxProcessor and OutboxProcessor can be configured via HopperOptions.
- Inbox processing defines where work messages arrive and where failure messages go.
- Outbox processing acts as a staging queue, ensuring atomic dispatch in distributed transaction boundaries.
{
"Shuttle": {
"Hopper": {
"Inbox": {
"WorkTransportUri": "queue://inbox-work",
"ErrorTransportUri": "queue://inbox-error",
"ThreadCount": 5
},
"Outbox": {
"WorkTransportUri": "queue://outbox-work",
"ErrorTransportUri": "queue://outbox-error"
}
}
}
}
Additional Inboxes
An endpoint may process one or more additional inbox work queues next to its primary Inbox, without an extra deployment. Each additional inbox is registered by name using AddInbox and gets its own dedicated processor threads and idle back-off, so a busy queue does not starve another. There is no precedence between the queues.
services
.AddHopper(options =>
{
configuration.GetSection(HopperOptions.SectionName).Bind(options);
})
.AddInbox("priority");
{
"Shuttle": {
"Hopper": {
"Inbox": {
"WorkTransportUri": "azuresq://azure/my-server-work",
"DeferredTransportUri": "azuresq://azure/my-server-deferred",
"ErrorTransportUri": "azuresq://azure/shuttle-error"
},
"AdditionalInboxes": {
"priority": {
"WorkTransportUri": "azuresq://azure/my-server-work-priority",
"ThreadCount": 2
}
}
}
}
}
The options may also be set in code, with or without a configuration entry:
.AddInbox("priority", options => options.WorkTransportUri = new("azuresq://azure/my-server-work-priority"));
The following rules apply to an additional inbox:
- The primary
Inbox.WorkTransportUriis required whenever additional inboxes exist. WorkTransportUriis required, and neither it norDeferredTransportUrimay be any other transport uri used by the endpoint (the primary inbox's work, deferred and error transports, the outbox's transports, or another additional inbox's work, deferred or error transport).ErrorTransportUriis optional; when it is not set the primary inbox's error transport is used.DeferredTransportUriis optional:- when it is not set, deferred messages are parked on the primary inbox's deferred transport, and
DeferredMessageProcessorResetIntervalandDeferredMessageProcessorIdleDurationare ignored; - when it is set, the additional inbox gets its own deferred message processor thread that uses its own
DeferredMessageProcessorResetIntervalandDeferredMessageProcessorIdleDuration, and an error transport (its own or that of the primary inbox) is required. The primary inbox does not need a deferred transport in this case.
- when it is not set, deferred messages are parked on the primary inbox's deferred transport, and
ThreadCount,MaximumFailureCount,IdleDurationsandIgnoreOnFailureDurationsbehave as they do for the primary inbox.- Inbox names are case-insensitive, and an
AdditionalInboxesentry that has not been registered usingAddInboxcauses the bus to fail on start.
A message may be sent to an additional inbox of the sending endpoint by name:
await bus.SendAsync(new ProcessOrder(), builder => builder.ToInbox("priority"));
The following semantics apply:
SenderInboxWorkTransportUriandToSelf()use the primary inbox, which remains the endpoint's identity. Replies to messages taken from an additional inbox therefore arrive on the primary inbox, and a handler that sends a message usingToSelf()sends it to the primary inbox.- Subscriptions and published events target the primary inbox only. Additional inboxes are intended for direct sends using
ToInbox,WithRecipientor message routes. - A deferred message (including a failed message that is retried after an ignore duration) that is parked on the primary inbox's deferred transport is returned to the additional inbox whose work transport uri matches the message's
RecipientInboxWorkTransportUri; any other message is returned to the primary inbox. The match is on the work transport'sUri, which a transport may normalise (for instance by replacing a.host with the machine name), so a recipient written in another form (or, for aresolver://inbox, using the resolved uri) does not match, and the path is case-sensitive.ToInboxalways produces a matching recipient. A message placed on an additional inbox queue with a different or empty recipient moves to the primary inbox after its first deferral. An additional inbox with its own deferred transport always receives its deferred messages back. - Additional inbox thread pools use the service key
InboxProcessor:{name}, and an additional inbox's own deferred message processor usesDeferredMessageProcessor:{name}, with the name lower-cased; these are visible in theThreadingOptionsevents.
Deferred Messages
If an application requires messages to be deferred and processed at a later time, you can configure the DeferredTransportUri in your InboxOptions. Shuttle.Hopper will actively monitor this endpoint using a DeferredMessageProcessor to pick up the deferred messages when appropriate.
Message Routing
For outbound commands (SendAsync), the bus determines the correct destination through an IMessageRouteProvider. You can route messages by configuring MessageRouteOptions with matching specifications (e.g., regex matching, starts-with matching, specific assemblies, or explicit type lists):
{
"Shuttle": {
"Hopper": {
"MessageRoutes": [
{
"Uri": "queue://external-service",
"Specifications": [
{
"Name": "StartsWith",
"Value": "MyCompany.Messages"
},
{
"Name": "Assembly",
"Value": "MyCompany.Messages.Assembly"
}
]
}
]
}
}
}
Bus Control
The IBusControl interface is used to start and stop the bus dynamically.
IBusControl
public interface IBusControl : IDisposable, IAsyncDisposable
{
bool Started { get; }
Task<IBusControl> StartAsync(CancellationToken cancellationToken = default);
Task StopAsync(CancellationToken cancellationToken = default);
}
.NET Generic Host Support
Shuttle.Hopper integrates elegantly into the standard .NET IHostedService lifecycle. If the AutoStart option is set to true (which is the default), a registered BusHostedService automatically handles starting and stopping the bus alongside your application host.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.Caching.Memory (>= 10.0.12)
- Microsoft.Extensions.DependencyInjection (>= 10.0.12)
- Microsoft.Extensions.Hosting (>= 10.0.12)
- Shuttle.Contract (>= 21.0.1)
- Shuttle.Pipelines (>= 21.0.3)
- Shuttle.Platform (>= 21.0.1)
- Shuttle.Reflection (>= 21.0.1)
- Shuttle.Serialization (>= 21.0.1)
- Shuttle.Specification (>= 21.0.1)
- Shuttle.Streams (>= 21.0.1)
- Shuttle.Threading (>= 21.0.4)
NuGet packages (9)
Showing the top 5 NuGet packages that depend on Shuttle.Hopper:
| Package | Downloads |
|---|---|
|
Shuttle.Hopper.AzureStorageQueues
Azure Storage Queue implementation for use with Shuttle.Hopper. |
|
|
Shuttle.Hopper.AmazonSqs
Amazon Simple Queue Service implementation for use with Shuttle.Hopper. |
|
|
Shuttle.Hopper.SqlServer.Queue
Provides a Sql Server Queue implementation for use with Shuttle.Hopper. |
|
|
Shuttle.Hopper.RabbitMQ
RabbitMQ implementation for use with Shuttle.Hopper. |
|
|
Shuttle.Hopper.AzureEventHubs
Azure Event Hubs implementation for use with Shuttle.Hopper. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 22.1.0 | 0 | 10/7/2026 |
| 22.0.3 | 276 | 9/6/2026 |
| 22.0.2 | 133 | 9/6/2026 |
| 22.0.1 | 222 | 8/17/2026 |
| 22.0.0-alpha.1 | 182 | 8/13/2026 |
| 21.0.2 | 862 | 4/17/2026 |
| 21.0.1 | 196 | 4/15/2026 |
| 21.0.1-rc3 | 276 | 4/11/2026 |
| 21.0.1-rc2 | 230 | 3/21/2026 |
| 21.0.1-rc1 | 212 | 2/28/2026 |
| 21.0.1-beta | 212 | 2/7/2026 |
| 21.0.0-alpha | 189 | 1/18/2026 |