Uniphar.ServiceBus.Extensions 3.0.0

dotnet add package Uniphar.ServiceBus.Extensions --version 3.0.0
                    
NuGet\Install-Package Uniphar.ServiceBus.Extensions -Version 3.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Uniphar.ServiceBus.Extensions" Version="3.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Uniphar.ServiceBus.Extensions" Version="3.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Uniphar.ServiceBus.Extensions" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Uniphar.ServiceBus.Extensions --version 3.0.0
                    
#r "nuget: Uniphar.ServiceBus.Extensions, 3.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Uniphar.ServiceBus.Extensions@3.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Uniphar.ServiceBus.Extensions&version=3.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Uniphar.ServiceBus.Extensions&version=3.0.0
                    
Install as a Cake Tool

ServiceBus Extensions

NuGet package repository for ServiceBus Extensions.

Provides three main capabilities:

  • Subscription management — declare topics and subscriptions with filters entirely from appsettings.json
  • Event handler — process messages from a subscription with a typed IMessageHandler<T>, with built-in settlement, poison-message dead-lettering, and dependency-failure circuit breaking
  • Operator CLI — inspect and replay dead-lettered messages from any DLQ via uniphar-sb

The producer owns the topic; the consumer owns the subscription.


Subscription Management

Register an administration client and the options:

builder.Services.Configure<SubscriberOptions>(builder.Configuration.GetSection(nameof(SubscriberOptions)));
builder.Services.Configure<TopicOptions>(builder.Configuration.GetSection(nameof(TopicOptions)));
builder.Services.AddSingleton(
    new ServiceBusAdministrationClient(builder.Configuration["AzureServiceBus:Name"]!,
    new DefaultAzureCredential()));

Then call InitializeAsync at startup to create or update topics and subscriptions:

await topicOptions.InitializeAsync(serviceBusAdministrationClient);
await subscriberOptions.InitializeAsync(serviceBusAdministrationClient);

On InitializeAsync: new rules are created, rules no longer in configuration are removed, and existing rules are updated to match.

Topic configuration

"TopicOptions": {
  "Topics": [
    {
      "Name": "topic-name"
    }
  ]
}

Subscription configuration

"SubscriberOptions": {
  "SubscriptionKey": {
    "Name": "subscription-name",
    "TopicName": "topic-name",
    "MaxDeliveryCount": 20,
    "CorrelationRules": [
      {
        "Name": "filter-name",
        "Subject": "subject-name"
      }
    ]
  }
}

Subscription rules

Each subscription can have one or more CorrelationRule or SqlRule entries.

CorrelationRule

Maps to the properties of CorrelationRuleFilter. Supports optional ApplicationProperties as key-value pairs:

"CorrelationRules": [
  {
    "Name": "CorrelationRule",
    "Subject": "MessageSubject",
    "ApplicationProperties": {
      "object_type": "ObjectType",
      "event_type": "Created"
    }
  }
]
SqlRule

Maps to SqlRuleFilter. Name and Filter are required; Action is optional:

"SqlRules": [
  {
    "Name": "SqlRule",
    "Filter": "color='blue' AND quantity=10",
    "Action": "SET quantity = quantity / 2;"
  }
]

Event Handler

Register processors and senders:

var serviceBusName = builder.Configuration["AzureServiceBus:Name"]!;
builder.AddServiceBusProcessors(serviceBusName, jsonSerializerOptions);

Sender

Add a SenderOptions section in appsettings.json:

"SenderOptions": [
  {
    "Identifier": "sender-id",
    "QueueOrTopicName": "topic-name"
  }
]

Register and inject:

builder.AddServiceBusProcessors(...)
    .AddSender("sender-id");
[FromKeyedServices("sender-id")]
ServiceBusSender serviceBusSender

Processor

Add a Processor block inside the relevant subscriber options:

"Processor": {
  "ProcessingType": "Topic",
  "MaxConcurrentCalls": 10
}

Implement IMessageHandler<T> and register:

builder.AddServiceBusProcessors(...)
    .AddProcessor<MyMessageHandler, MyMessage>("subscription-name");

This registers a HostedService that receives messages from the subscription and dispatches each one to MyMessageHandler.

Message handler contract

HandleAsync must return an IMessageResult:

MessageResult.Complete();        // message processed successfully
MessageResult.Abandon();         // requeue for redelivery
MessageResult.DeadLetter("reason", "optional description");

Behaviour:

  • If the message body cannot be deserialized into TMessage, the message is dead-lettered immediately with reason PoisonMessage, and the handler is not invoked.
  • Complete() → the message is completed.
  • Abandon() → the message is abandoned and may be redelivered by Service Bus.
  • DeadLetter(reason, description) → the message is moved to the DLQ immediately with the supplied reason and optional description.
  • Thrown dependency exceptions are handled specially:
    • HttpRequestException → counted as a dependency failure; once the message reaches MaxDeliveryCount, it is dead-lettered with reason HttpRequestFailed.
    • TaskCanceledException (when the processor itself is not stopping) and Polly TimeoutRejectedException → counted as dependency failures; once the message reaches MaxDeliveryCount, it is dead-lettered with reason HttpRequestTimeout.
    • Before MaxDeliveryCount is reached, those exceptions are rethrown so Service Bus can retry the message normally.
  • MaxDeliveryCount is resolved from the Service Bus entity metadata at startup. If that lookup fails, the processor falls back to the subscription configuration value, or 10 if none is configured.
  • A successful HandleAsync call resets the dependency-failure streak.
  • Parse failures and explicit DeadLetter(...) results do not open the circuit breaker.
  • When consecutive dependency failures reach Processor.MaxConsecutiveFailureCount (default 5), the processor stops, waits Processor.CircuitBreakerResetIntervalInSeconds (default 30 seconds), then starts again unless the host is shutting down.

Dead-lettered messages remain in the DLQ for manual inspection and replay.
For duplicate suppression, prefer native Service Bus duplicate detection on the source queue or topic.

Logs:

Level Message
ERROR An error occurred while processing events in {ErrorSource}
ERROR An error occurred while deserializing message {MessageId}
WARN {MessageType} dependency failure streak increased to {FailureCount}.
WARN Processor reached the maximum consecutive failure count of {FailureCount} and will stop.
WARN Message lock lost while attempting to {Operation} message {MessageId}. The message will be redelivered by Service Bus.

Additional lifecycle logs are emitted when the processor starts, stops, resumes after a circuit-breaker pause, or skips a resume because shutdown has started.


Dead-Message Replay

Library integration

Register replay services:

builder.AddServiceBusReplayServices(serviceBusName);

Required configuration:

{
  "AzureServiceBus": {
    "Name": "my-namespace.servicebus.windows.net"
  }
}

This registers:

  • IDeadMessageStore — list and fetch dead-letter messages from a DLQ
  • IDeadMessageReplayer — validate and replay a dead-letter message

Replay preserves the original payload and key headers, generates a new MessageId, and stores the original ID in the x-original-message-id application property.

EntityPath values must be either <queue> or <topic>/subscriptions/<subscription>.

List dead-letter messages
var store = services.GetRequiredService<IDeadMessageStore>();
var messages = await store.ListAsync(new DeadMessageQuery
{
    EntityPath = "orders/subscriptions/worker",
    DeadLetterReason = "PoisonMessage",
    FromEnqueuedTime = DateTimeOffset.Parse("2026-01-01T00:00:00Z"),
    Limit = 10
});
Replay a message
var replayer = services.GetRequiredService<IDeadMessageReplayer>();
var result = await replayer.ReplayAsync(new DeadMessageReplayRequest
{
    EntityPath = "orders/subscriptions/worker",
    MessageId = "message-123",
    DryRun = true
});

DryRun = true validates and builds the outbound message shape without sending it.


Operator CLI (uniphar-sb)

A .NET tool for inspecting and replaying dead-letter messages against a live Service Bus namespace.

Install
# global
dotnet tool install --global Uniphar.ServiceBus.Cli --add-source .\src\Uniphar.ServiceBus.Cli\nupkg

# local
dotnet new tool-manifest
dotnet tool install Uniphar.ServiceBus.Cli --add-source .\src\Uniphar.ServiceBus.Cli\nupkg

Or run directly from the repo without installing:

dotnet run --project .\src\Uniphar.ServiceBus.Cli -- <command> [options]
Commands
dlq list    -n <namespace> (-e <entity> | -t <topic> -u <subscription>) [-f <timestamp>] [-b <timestamp>] [-r <reason>] [-l <n>]
dlq show    -n <namespace> (-e <entity> | -t <topic> -u <subscription>) (-m <id> | -s <n>)
dlq replay  -n <namespace> (-e <entity> | -t <topic> -u <subscription>) (-s <n> | [-m <id>] [-f <timestamp>] [-b <timestamp>] [-r <reason>]) [-a <entity>] [-d]
dlq seed    -n <namespace> (-e <entity> | -t <topic> -u <subscription>) [-c <n>] [-r <reason>] [-y <entity>] [-p <prefix>]

messages list  -n <namespace> (-e <entity> | -t <topic> -u <subscription>) [-f <timestamp>] [-b <timestamp>] [-l <n>]
messages show  -n <namespace> (-e <entity> | -t <topic> -u <subscription>) (-m <id> | -s <n>)
Examples
# list dead-letter messages
uniphar-sb dlq list -n my-namespace.servicebus.windows.net -e orders/subscriptions/worker -r PoisonMessage -f 2026-01-01T00:00:00Z -l 10

# inspect one message by sequence number
uniphar-sb dlq show -n my-namespace.servicebus.windows.net -e orders/subscriptions/worker -s 42

# replay all messages matching a reason filter (dry run)
uniphar-sb dlq replay -n my-namespace.servicebus.windows.net -e orders/subscriptions/worker -r PoisonMessage -f 2026-01-01T00:00:00Z -d

# replay a single message by sequence number to a different target
uniphar-sb dlq replay -n my-namespace.servicebus.windows.net -e orders/subscriptions/worker -s 42 -a orders
Options reference
Flag Description
-n / --service-bus-namespace Namespace (bare name or FQDN — bare names are expanded automatically)
-e / --entity Entity path: <queue> or <topic>/subscriptions/<subscription>
-t / --topic + -u / --subscription Alternative to -e for topic subscriptions
-m / --message-id Select a single message by ID
-s / --sequence-number Select a single message by DLQ sequence number (exact)
-f / --from-date Filter from this enqueued timestamp (inclusive)
-b / --to-date Filter up to this enqueued timestamp; defaults to now when -f is supplied
-r / --dead-letter-reason Filter by dead-letter reason
-l / --limit Maximum number of messages to return
-a / --target Override replay destination; defaults to ReplyTo header, then original entity
-d / --dry-run Validate and build the replay message without sending
Replay behaviour
  • -s or -m alone → replay exactly one message.
  • -f, -b, or -r → replay every matching DLQ message.
  • -a / --target overrides the destination; without it, replay uses the ReplyTo header or falls back to the original entity path.
  • Replay is rejected if the message is missing required replay data.
  • A new MessageId is generated to avoid duplicate-detection collisions; the original is stored in x-original-message-id.
  • The source message is not removed from the DLQ automatically.

Testing

# unit tests (fast, no Azure required)
dotnet test .\src\Uniphar.ServiceBus.Extensions.UnitTests\Uniphar.ServiceBus.Extensions.UnitTests.csproj

# Azure-backed end-to-end tests (requires access to the dev Key Vault and integration entities)
dotnet test .\src\Uniphar.ServiceBus.Extensions.E2ETests\Uniphar.ServiceBus.Extensions.E2ETests.csproj
Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
3.0.0 90 5/6/2026
2.9.0 11,738 2/27/2026
2.8.4 21,694 10/23/2025
2.8.3 3,760 9/26/2025
2.8.2 189 9/26/2025
2.8.1 300 9/25/2025
2.8.0 2,793 8/28/2025
2.7.0 874 8/26/2025
2.6.0 542 8/19/2025
2.5.0 221 8/19/2025
2.4.2 7,282 4/8/2025
2.4.1 310 4/3/2025
2.4.0 538 3/31/2025
2.3.0 1,624 2/13/2025
2.2.0 407 2/7/2025
2.1.0 279 2/6/2025
2.0.0 195 2/5/2025
1.0.0 181 1/30/2025