├── .reposync.yml
├── reset-virtual-host.cmd
├── scripts
└── Reset-RabbitMQ.ps1
├── CONTRIBUTING.md
├── src
├── NServiceBus.snk
├── NServiceBusTests.snk
├── NServiceBus.Transport.RabbitMQ.CommandLine
│ ├── .editorconfig
│ ├── RoutingTopologyType.cs
│ ├── BrokerConnection.cs
│ ├── RoutingTopologyBinder.cs
│ ├── Properties
│ │ └── launchSettings.json
│ ├── Commands
│ │ ├── Delays
│ │ │ ├── DelaysVerifyCommand.cs
│ │ │ └── DelaysCreateCommand.cs
│ │ └── Queue
│ │ │ └── QueueValidateDeliveryLimitCommand.cs
│ ├── Program.cs
│ ├── BrokerVerifierBinder.cs
│ ├── NServiceBus.Transport.RabbitMQ.CommandLine.csproj
│ └── BrokerConnectionBinder.cs
├── Directory.Build.targets
├── Custom.Build.props
├── .globalconfig
├── NServiceBus.Transport.RabbitMQ
│ ├── Administration
│ │ ├── ManagementApi
│ │ │ ├── Models
│ │ │ │ ├── PolicyTarget.cs
│ │ │ │ ├── QueueMessageStats.cs
│ │ │ │ ├── QueueArguments.cs
│ │ │ │ ├── PolicyDefinition.cs
│ │ │ │ ├── QueueType.cs
│ │ │ │ ├── Overview.cs
│ │ │ │ ├── FeatureFlagExtensions.cs
│ │ │ │ ├── FeatureFlag.cs
│ │ │ │ ├── GetQueuesResult.cs
│ │ │ │ ├── Policy.cs
│ │ │ │ ├── Binding.cs
│ │ │ │ └── Queue.cs
│ │ │ └── Converters
│ │ │ │ ├── FeatureFlagStateConverter.cs
│ │ │ │ ├── DeliveryLimitConverter.cs
│ │ │ │ ├── QueueTypeConverter.cs
│ │ │ │ └── PolicyTargetConverter.cs
│ │ ├── QueuePurger.cs
│ │ └── SubscriptionManager.cs
│ ├── Configuration
│ │ ├── PrefetchCountCalculation.cs
│ │ ├── QueueType.cs
│ │ ├── BrokerRequirementChecks.cs
│ │ ├── CertificateLoader.cs
│ │ ├── RoutingTopology.cs
│ │ └── ManagementApiConfiguration.cs
│ ├── ExceptionExtensions.cs
│ ├── Receiving
│ │ ├── ModelExtensions.cs
│ │ └── MessagePumpConnectionFailedCircuitBreaker.cs
│ ├── NServiceBus.Transport.RabbitMQ.csproj
│ ├── Sending
│ │ ├── NonPersistentDeliveryModeExtensions.cs
│ │ ├── MessageDispatcher.cs
│ │ └── BasicPropertiesExtensions.cs
│ ├── Routing
│ │ └── DefaultRoutingKeyConvention.cs
│ ├── Connection
│ │ └── ConfirmsAwareChannel.cs
│ └── RabbitMQTransportInfrastructure.cs
├── NServiceBus.Transport.RabbitMQ.TransportTests
│ ├── .editorconfig
│ ├── TransportTestsConfiguration.cs
│ ├── NServiceBus.Transport.RabbitMQ.TransportTests.csproj
│ ├── When_delayed_delivery_is_disabled.cs
│ ├── ConnectionHelper.cs
│ ├── ConfigureRabbitMQTransportInfrastructure.cs
│ ├── When_changing_concurrency.cs
│ └── When_modifying_incoming_headers.cs
├── NServiceBus.Transport.RabbitMQ.AcceptanceTests
│ ├── ConfigurationHelpers.cs
│ ├── QuorumEndpoint.cs
│ ├── .editorconfig
│ ├── TestSuiteConstraints.cs
│ ├── ChannelExtensions.cs
│ ├── NServiceBus.Transport.RabbitMQ.AcceptanceTests.csproj
│ ├── ConnectionHelper.cs
│ ├── QuorumQueues
│ │ ├── When_quorum_endpoint_uses_classic_queue.cs
│ │ ├── When_classic_endpoint_uses_quorum_queue.cs
│ │ ├── When_classic_endpoint_uses_quorum_error_queue.cs
│ │ ├── When_quorum_endpoint_uses_classic_error_queue.cs
│ │ ├── When_immediate_retries_with_quorum_queues.cs
│ │ └── When_starting_endpoint_using_quorum_queues.cs
│ ├── DelayedDelivery
│ │ └── When_deferring_a_message_longer_than_allowed_maximum.cs
│ ├── When_using_direct_routing.cs
│ ├── When_using_a_custom_message_id_strategy_that_returns_an_invalid_message_id.cs
│ ├── When_receiving_a_message.cs
│ ├── When_requesting_non_persistent_delivery.cs
│ ├── When_customizing_outgoing_messages.cs
│ ├── When_scaling_out_subscribers.cs
│ ├── ConfigureEndpointRabbitMQTransport.cs
│ ├── When_publishing_message_implementing_interface_in_direct_topology.cs
│ ├── When_the_message_contains_a_legacy_callback_header.cs
│ ├── When_receiving_a_reply_that_contains_a_legacy_callback_header.cs
│ ├── When_the_broker_connection_is_lost.cs
│ └── When_using_a_custom_message_id_strategy.cs
├── targets
│ ├── Targets.csproj
│ ├── Program.cs
│ └── Broker.cs
├── NServiceBus.Transport.RabbitMQ.CommandLine.Tests
│ ├── .editorconfig
│ ├── NServiceBus.Transport.RabbitMQ.CommandLine.Tests.csproj
│ └── MigrateDelayedMessages
│ │ └── When_migrating_a_delayed_message.cs
├── NServiceBus.Transport.RabbitMQ.Tests
│ ├── .editorconfig
│ ├── APIApprovals.cs
│ ├── When_stopping_endpoint.cs
│ ├── NServiceBus.Transport.RabbitMQ.Tests.csproj
│ ├── Support
│ │ └── ConventionalRoutingTopologyExtensions.cs
│ ├── DelayedDelivery
│ │ └── When_calculating_a_routing_key.cs
│ ├── When_consuming_from_classic_queues.cs
│ ├── OutgoingMessageBuilder.cs
│ ├── Connection
│ │ └── ConnectionConfigurationWithAmqpTests.cs
│ └── RabbitMqContext.cs
├── NServiceBus.Transport.RabbitMQ.slnx
├── msbuild
│ ├── ConvertToVersionRange.cs
│ └── AutomaticVersionRanges.targets
└── Directory.Build.props
├── .gitattributes
├── .github
├── SUPPORT.md
├── workflows
│ ├── nuget-audit.yml
│ ├── code-analysis.yml
│ ├── stale.yml
│ ├── virus-scan.yml
│ ├── update-dependencies.yml
│ ├── release.yml
│ └── ci.yml
└── ISSUE_TEMPLATE
│ ├── config.yml
│ ├── feature_request.yml
│ ├── improvement_request.yml
│ └── bug_report.yml
├── global.json
├── .repointegrity.yml
├── SECURITY.md
├── .mailmap
├── nuget.config
├── LICENSE.md
├── .gitignore
├── Package-README.md
└── README.md
/.reposync.yml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/reset-virtual-host.cmd:
--------------------------------------------------------------------------------
1 | @echo Off
2 | cd %~dp0
3 |
4 | dotnet run --project src/targets -- %*
5 |
--------------------------------------------------------------------------------
/scripts/Reset-RabbitMQ.ps1:
--------------------------------------------------------------------------------
1 | rabbitmqctl stop_app
2 | rabbitmqctl reset
3 | rabbitmqctl start_app
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | For information on contributing, see https://docs.particular.net/platform/contributing.
2 |
--------------------------------------------------------------------------------
/src/NServiceBus.snk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Particular/NServiceBus.RabbitMQ/HEAD/src/NServiceBus.snk
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | # Auto detect text files and perform LF normalization
2 | * text=auto
3 | *.sh text eol=lf
4 |
--------------------------------------------------------------------------------
/.github/SUPPORT.md:
--------------------------------------------------------------------------------
1 | # Looking for Support
2 |
3 | Check out our [support options](https://particular.net/support).
--------------------------------------------------------------------------------
/src/NServiceBusTests.snk:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Particular/NServiceBus.RabbitMQ/HEAD/src/NServiceBusTests.snk
--------------------------------------------------------------------------------
/global.json:
--------------------------------------------------------------------------------
1 | {
2 | "sdk": {
3 | "version": "10.0.0",
4 | "allowPrerelease": false,
5 | "rollForward": "latestFeature"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.CommandLine/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.cs]
2 |
3 | # Justification: Application
4 | dotnet_diagnostic.CA2007.severity = none
5 |
--------------------------------------------------------------------------------
/src/Directory.Build.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/Custom.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 11.0
5 | minor
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/.globalconfig:
--------------------------------------------------------------------------------
1 | is_global = true
2 |
3 | dotnet_analyzer_diagnostic.severity = none
4 |
5 | # Workaround for https://github.com/dotnet/roslyn/issues/41640
6 | dotnet_diagnostic.EnableGenerateDocumentationFile.severity = none
7 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.CommandLine/RoutingTopologyType.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.CommandLine
2 | {
3 | enum RoutingTopologyType
4 | {
5 | Conventional,
6 | Direct
7 | }
8 | }
--------------------------------------------------------------------------------
/.repointegrity.yml:
--------------------------------------------------------------------------------
1 | ignore:
2 | # Internals need to be visible to ServiceControl RabbitMQ transport plugin
3 | - test: NoInternalsVisibleToForNonexistantProjects
4 | path: src/NServiceBus.Transport.RabbitMQ/NServiceBus.Transport.RabbitMQ.csproj
--------------------------------------------------------------------------------
/.github/workflows/nuget-audit.yml:
--------------------------------------------------------------------------------
1 | name: NuGet Audit
2 | on:
3 | workflow_dispatch:
4 | env:
5 | DOTNET_NOLOGO: true
6 | jobs:
7 | call-shared-nuget-audit:
8 | uses: particular/shared-workflows/.github/workflows/nuget-audit.yml@main
9 | secrets: inherit
10 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Administration/ManagementApi/Models/PolicyTarget.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.ManagementApi;
2 |
3 | enum PolicyTarget
4 | {
5 | All,
6 | Queues,
7 | ClassicQueues,
8 | QuorumQueues,
9 | Streams,
10 | Exchanges,
11 | }
12 |
--------------------------------------------------------------------------------
/.github/workflows/code-analysis.yml:
--------------------------------------------------------------------------------
1 | name: Code Analysis
2 | on:
3 | push:
4 | branches:
5 | - main
6 | - master
7 | - release-*
8 | pull_request:
9 | workflow_dispatch:
10 | jobs:
11 | code-analysis:
12 | uses: particular/shared-workflows/.github/workflows/code-analysis.yml@main
13 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.TransportTests/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.cs]
2 |
3 | # Justification: Test project
4 | dotnet_diagnostic.CA2007.severity = none
5 |
6 | # Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken
7 | dotnet_diagnostic.NSB0002.severity = suggestion
8 |
--------------------------------------------------------------------------------
/.github/workflows/stale.yml:
--------------------------------------------------------------------------------
1 | name: StaleBot
2 | on:
3 | workflow_dispatch:
4 | schedule:
5 | - cron: "0 0 * * *"
6 | jobs:
7 | stalebot:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - name: Mark stale PRs
11 | uses: Particular/stale-action@main
12 | with:
13 | repo-token: ${{ secrets.GITHUB_TOKEN }}
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.TransportTests/TransportTestsConfiguration.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.TransportTests;
2 |
3 | public partial class TransportTestsConfiguration : ITransportTestsConfiguration
4 | {
5 | public IConfigureTransportInfrastructure CreateTransportConfiguration() => new ConfigureRabbitMQTransportInfrastructure();
6 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: false
2 | contact_links:
3 | - name: Ask a question
4 | url: https://discuss.particular.net/
5 | about: Reach out to the ParticularDiscussion community.
6 | - name: Get support
7 | url: https://particular.net/support
8 | about: Contact us to discuss your support requirements.
9 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | Particular Software takes security issues very seriously. We appreciate your efforts to uncover bugs in our software.
4 |
5 | ## Reporting a Vulnerability
6 |
7 | Vulnerabilities can be reported by [submitting an issue](https://github.com/Particular/NServiceBus/security/advisories/new) through the security tab on our NServiceBus repository.
8 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Administration/ManagementApi/Models/QueueMessageStats.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | namespace NServiceBus.Transport.RabbitMQ.ManagementApi;
4 |
5 | using System.Text.Json.Serialization;
6 |
7 | // For ServiceControl licensing component
8 | class QueueMessageStats
9 | {
10 | [JsonPropertyName("ack")]
11 | public long? Ack { get; set; }
12 | }
13 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/ConfigurationHelpers.cs:
--------------------------------------------------------------------------------
1 | using NServiceBus;
2 | using NServiceBus.AcceptanceTests.EndpointTemplates;
3 |
4 | static class ConfigurationHelpers
5 | {
6 | public static RabbitMQTransport ConfigureRabbitMQTransport(this EndpointConfiguration configuration)
7 | {
8 | return (RabbitMQTransport)configuration.ConfigureTransport();
9 | }
10 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Administration/ManagementApi/Models/QueueArguments.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | namespace NServiceBus.Transport.RabbitMQ.ManagementApi;
4 |
5 | using System.Text.Json.Serialization;
6 |
7 | class QueueArguments
8 | {
9 | [JsonPropertyName("x-delivery-limit")]
10 | [JsonConverter(typeof(DeliveryLimitConverter))]
11 | public int? DeliveryLimit { get; set; }
12 | }
13 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Administration/ManagementApi/Models/PolicyDefinition.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | namespace NServiceBus.Transport.RabbitMQ.ManagementApi;
4 |
5 | using System.Text.Json.Serialization;
6 |
7 | class PolicyDefinition
8 | {
9 | [JsonPropertyName("delivery-limit")]
10 | [JsonConverter(typeof(DeliveryLimitConverter))]
11 | public int? DeliveryLimit { get; set; }
12 | }
13 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Administration/ManagementApi/Models/QueueType.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.ManagementApi;
2 |
3 | // Note that this is different to `NServiceBus.QueueType` which lists the types of queues supported
4 | // by the NServiceBus transport, which doesn't include the `Stream` value
5 | enum QueueType
6 | {
7 | Unknown,
8 | Classic,
9 | Quorum,
10 | Stream,
11 | MqttQos0
12 | }
13 |
--------------------------------------------------------------------------------
/src/targets/Targets.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net10.0
5 | Exe
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/QuorumEndpoint.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests
2 | {
3 | using NServiceBus.AcceptanceTests.EndpointTemplates;
4 |
5 | class QuorumEndpoint : DefaultServer
6 | {
7 | public QuorumEndpoint()
8 | {
9 | var transportConfiguration = new ConfigureEndpointRabbitMQTransport(QueueType.Quorum);
10 | TransportConfiguration = transportConfiguration;
11 | }
12 | }
13 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Configuration/PrefetchCountCalculation.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus
2 | {
3 | ///
4 | /// Calculates the value for the prefetch count based on the endpoint's maximum concurrency setting.
5 | ///
6 | /// Maximum concurrency of the message receiver.
7 | /// The prefetch count to use for the receiver.
8 | public delegate long PrefetchCountCalculation(int maximumConcurrency);
9 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Administration/ManagementApi/Models/Overview.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | namespace NServiceBus.Transport.RabbitMQ.ManagementApi;
4 |
5 | using System.Text.Json.Serialization;
6 |
7 | class Overview
8 | {
9 | [JsonPropertyName("rabbitmq_version")]
10 | public required string BrokerVersion { get; set; }
11 |
12 | // For ServiceControl licensing component
13 | [JsonPropertyName("disable_stats")]
14 | public required bool DisableStats { get; set; }
15 | }
16 |
--------------------------------------------------------------------------------
/.mailmap:
--------------------------------------------------------------------------------
1 | Andreas Öhlund Andreas Öhlund
2 | Andreas Öhlund andreasohlund
3 | Andreas Öhlund andreasohlund
4 | danielmarbach Daniel Marbach
5 | John Simons John Simons
6 | Szymon Kulec Scooletz
7 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.cs]
2 |
3 | # Justification: Test project
4 | dotnet_diagnostic.CA2007.severity = none
5 |
6 | # may be enabled in future
7 | dotnet_diagnostic.PS0018.severity = none # A task-returning method should have a CancellationToken parameter unless it has a parameter implementing ICancellableContext
8 |
9 | # Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken
10 | dotnet_diagnostic.NSB0002.severity = suggestion
11 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Administration/ManagementApi/Models/FeatureFlagExtensions.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | namespace NServiceBus.Transport.RabbitMQ.ManagementApi;
4 |
5 | using System;
6 | using System.Collections.Generic;
7 | using System.Linq;
8 |
9 | static class FeatureFlagExtensions
10 | {
11 | public static bool HasEnabledFeature(this List list, string featureName) =>
12 | list.Any(feature => feature.Name.Equals(featureName, StringComparison.OrdinalIgnoreCase) && feature.Enabled);
13 | }
14 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Administration/ManagementApi/Models/FeatureFlag.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | namespace NServiceBus.Transport.RabbitMQ.ManagementApi;
4 |
5 | using System.Text.Json.Serialization;
6 |
7 | class FeatureFlag
8 | {
9 | [JsonPropertyName("name")]
10 | public required string Name { get; set; }
11 |
12 | [JsonPropertyName("state")]
13 | [JsonConverter(typeof(FeatureFlagEnabledConverter))]
14 | public bool Enabled { get; set; }
15 |
16 | public const string StreamQueue = "stream_queue";
17 | }
18 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Administration/ManagementApi/Models/GetQueuesResult.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | namespace NServiceBus.Transport.RabbitMQ.ManagementApi;
4 |
5 | using System.Collections.Generic;
6 | using System.Text.Json.Serialization;
7 |
8 | // For ServiceControl licensing component
9 | class GetQueuesResult
10 | {
11 | [JsonPropertyName("items")]
12 | public required List Items { get; set; }
13 |
14 | [JsonPropertyName("page")]
15 | public required int Page { get; set; }
16 |
17 | [JsonPropertyName("page_count")]
18 | public required int PageCount { get; set; }
19 | }
20 |
--------------------------------------------------------------------------------
/.github/workflows/virus-scan.yml:
--------------------------------------------------------------------------------
1 | name: Virus scan
2 | on:
3 | release:
4 | types: [published]
5 | jobs:
6 | virus-scan:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Scan release for viruses
10 | uses: Particular/virus-scan-action@main
11 | with:
12 | owner: ${{ github.repository_owner }}
13 | repo: ${{ github.event.repository.name }}
14 | tag: ${{ github.event.release.name }}
15 | github-access-token: ${{ secrets.GITHUB_TOKEN }}
16 | slack-token: ${{ secrets.SLACK_TOKEN }}
17 | slack-channel: ${{ vars.VIRUS_REPORTING_SLACK_CHANNEL }}
18 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.CommandLine.Tests/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.cs]
2 |
3 | # Justification: Test project
4 | dotnet_diagnostic.CA2007.severity = none
5 |
6 | # may be enabled in future
7 | dotnet_diagnostic.PS0018.severity = none # A task-returning method should have a CancellationToken parameter unless it has a parameter implementing ICancellableContext
8 | dotnet_diagnostic.PS0004.severity = none # Make the CancellationToken parameter required
9 |
10 | # Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken
11 | dotnet_diagnostic.NSB0002.severity = suggestion
12 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/ExceptionExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus
2 | {
3 | using System;
4 | using System.Threading;
5 |
6 | static class ExceptionExtensions
7 | {
8 | #pragma warning disable PS0003 // A parameter of type CancellationToken on a non-private delegate or method should be optional
9 | public static bool IsCausedBy(this Exception ex, CancellationToken cancellationToken) => ex is OperationCanceledException && cancellationToken.IsCancellationRequested;
10 | #pragma warning restore PS0003 // A parameter of type CancellationToken on a non-private delegate or method should be optional
11 | }
12 | }
--------------------------------------------------------------------------------
/src/targets/Program.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 |
3 | using static Bullseye.Targets;
4 |
5 | static class Program
6 | {
7 | public static async Task Main(string[] args)
8 | {
9 | Target("delete-virtual-host", () => Broker.DeleteVirtualHost());
10 |
11 | Target("create-virtual-host", () => Broker.CreateVirtualHost());
12 |
13 | Target("add-user-to-virtual-host", () => Broker.AddUserToVirtualHost());
14 |
15 | Target("default", dependsOn: ["delete-virtual-host", "create-virtual-host", "add-user-to-virtual-host"]);
16 |
17 | await RunTargetsAndExitAsync(args).ConfigureAwait(true);
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.Tests/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.cs]
2 |
3 | # Justification: Test project
4 | dotnet_diagnostic.CA2007.severity = none
5 |
6 | # may be enabled in future
7 | dotnet_diagnostic.PS0004.severity = none # A parameter of type CancellationToken on a private delegate or method should be required
8 | dotnet_diagnostic.PS0018.severity = none # A task-returning method should have a CancellationToken parameter unless it has a parameter implementing ICancellableContext
9 |
10 | # Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken
11 | dotnet_diagnostic.NSB0002.severity = suggestion
12 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Administration/ManagementApi/Models/Policy.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | namespace NServiceBus.Transport.RabbitMQ.ManagementApi;
4 |
5 | using System.Text.Json.Serialization;
6 |
7 | class Policy
8 | {
9 | [JsonPropertyName("pattern")]
10 | public required string Pattern { get; set; }
11 |
12 | [JsonPropertyName("apply-to")]
13 | [JsonConverter(typeof(PolicyTargetConverter))]
14 | public required PolicyTarget ApplyTo { get; set; }
15 |
16 | [JsonPropertyName("definition")]
17 | public required PolicyDefinition Definition { get; set; }
18 |
19 | [JsonPropertyName("priority")]
20 | public int Priority { get; set; }
21 | }
22 |
--------------------------------------------------------------------------------
/nuget.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.CommandLine/BrokerConnection.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.CommandLine
2 | {
3 | using global::RabbitMQ.Client;
4 |
5 | class BrokerConnection(BrokerVerifier brokerVerifier, RabbitMQ.ConnectionFactory connectionFactory)
6 | {
7 | public async Task Create(CancellationToken cancellationToken = default)
8 | {
9 | await brokerVerifier.Initialize(cancellationToken);
10 | await brokerVerifier.VerifyRequirements(cancellationToken);
11 |
12 | var connection = await connectionFactory.CreateAdministrationConnection(cancellationToken);
13 |
14 | return connection;
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Administration/ManagementApi/Converters/FeatureFlagStateConverter.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | namespace NServiceBus.Transport.RabbitMQ.ManagementApi;
4 |
5 | using System;
6 | using System.Text.Json;
7 | using System.Text.Json.Serialization;
8 |
9 | class FeatureFlagEnabledConverter : JsonConverter
10 | {
11 | public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
12 | reader.GetString()?.Equals("enabled", StringComparison.OrdinalIgnoreCase) ?? false;
13 |
14 | public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) =>
15 | writer.WriteStringValue(value ? "enabled" : "disabled");
16 | }
17 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.Tests/APIApprovals.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.Tests
2 | {
3 | using NServiceBus;
4 | using NUnit.Framework;
5 | using Particular.Approvals;
6 | using PublicApiGenerator;
7 |
8 | [TestFixture]
9 | public class APIApprovals
10 | {
11 | [Test]
12 | public void Approve()
13 | {
14 | var publicApi = typeof(RabbitMQTransport).Assembly.GeneratePublicApi(new ApiGeneratorOptions
15 | {
16 | ExcludeAttributes = ["System.Runtime.Versioning.TargetFrameworkAttribute", "System.Reflection.AssemblyMetadataAttribute"]
17 | });
18 |
19 | Approver.Verify(publicApi);
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Configuration/QueueType.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus
2 | {
3 | ///
4 | /// The types of queues supported by the transport. Quorum queues should be considered the default option for a replicated queue type.
5 | ///
6 | public enum QueueType
7 | {
8 | ///
9 | /// The original type of queue provided by RabbitMQ. They should only be used when a non-replicated queue is desired.
10 | ///
11 | Classic,
12 |
13 | ///
14 | /// A queue that provides high availability via replication and focuses on data safety under network partition and failure scenarios.
15 | ///
16 | Quorum
17 | }
18 | }
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | By accessing code under the [Particular Software GitHub Organization](https://github.com/Particular) (Particular Software) here, you are agreeing to the following licensing terms.
2 | If you do not agree to these terms, do not access Particular Software code.
3 |
4 | Your license to Particular Software source code and/or binaries is governed by the Reciprocal Public License 1.5 (RPL1.5) license as described here:
5 |
6 | https://opensource.org/license/rpl-1-5/
7 |
8 | If you do not wish to release the source of software you build using Particular Software source code and/or binaries under the terms above, you may use Particular Software source code and/or binaries under the License Agreement described here:
9 |
10 | https://particular.net/LicenseAgreement
11 |
--------------------------------------------------------------------------------
/.github/workflows/update-dependencies.yml:
--------------------------------------------------------------------------------
1 | name: Update Dependencies
2 | on:
3 | schedule:
4 | # At 14 minutes past the hour, every 6 hours, starting at 3AM UTC
5 | - cron: '14 3/6 * * *'
6 | workflow_dispatch:
7 | inputs:
8 | dryRun:
9 | type: boolean
10 | required: false
11 | default: false
12 | description: Dry run (don't create PRs)
13 | debugMode:
14 | type: boolean
15 | required: false
16 | default: false
17 | description: Run Renovate with debug logging
18 | jobs:
19 | renovate:
20 | uses: particular/shared-workflows/.github/workflows/dependency-updates.yml@main
21 | with:
22 | dryRun: ${{ inputs.dryRun || false }}
23 | debugMode: ${{ inputs.debugMode || false }}
24 | secrets: inherit
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Configuration/BrokerRequirementChecks.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus;
2 |
3 | using System;
4 |
5 | //Any changes to BrokerRequirementChecks need to be synchronzied with the `all` value in BrokerVerifier.Initialize.
6 |
7 | ///
8 | /// The broker requirements that the transport will verify.
9 | ///
10 | [Flags]
11 | public enum BrokerRequirementChecks
12 | {
13 | ///
14 | /// None
15 | ///
16 | None = 0,
17 |
18 | ///
19 | /// The transport requires broker version 3.10 or newer.
20 | ///
21 | Version310OrNewer = 1,
22 |
23 | ///
24 | /// The 'stream-queue' feature flag needs to be enabled.
25 | ///
26 | StreamsEnabled = 2,
27 | }
28 |
29 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Administration/ManagementApi/Models/Binding.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | namespace NServiceBus.Transport.RabbitMQ.ManagementApi;
4 |
5 | using System.Text.Json.Serialization;
6 |
7 | // For ServiceControl licensing component
8 | class Binding
9 | {
10 | [JsonPropertyName("source")]
11 | public required string Source { get; set; }
12 |
13 | [JsonPropertyName("destination")]
14 | public required string Destination { get; set; }
15 |
16 | [JsonPropertyName("destination_type")]
17 | public required string DestinationType { get; set; }
18 |
19 | [JsonPropertyName("routing_key")]
20 | public required string RoutingKey { get; set; }
21 |
22 | [JsonPropertyName("properties_key")]
23 | public required string PropertiesKey { get; set; }
24 | }
25 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.Tests/When_stopping_endpoint.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.Tests
2 | {
3 | using System.Threading.Tasks;
4 | using NUnit.Framework;
5 |
6 | [TestFixture]
7 | class When_stopping_endpoint : RabbitMqContext
8 | {
9 | [Test]
10 | [Explicit]
11 | public async Task Should_gracefully_shutdown()
12 | {
13 | await messagePump.StopReceive();
14 |
15 | var operations = new OutgoingMessageBuilder().WithBody(new byte[1]).SendTo(ReceiverQueue).Build(10000);
16 | await messageDispatcher.Dispatch(operations, new TransportTransaction());
17 |
18 | await messagePump.StartReceive();
19 | await Task.Delay(500);
20 | await messagePump.StopReceive();
21 | }
22 | }
23 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/TestSuiteConstraints.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.AcceptanceTests
2 | {
3 | using AcceptanceTesting.Support;
4 |
5 | public partial class TestSuiteConstraints
6 | {
7 | public bool SupportsDtc => false;
8 |
9 | public bool SupportsCrossQueueTransactions => false;
10 |
11 | public bool SupportsNativePubSub => true;
12 |
13 | public bool SupportsDelayedDelivery => true;
14 |
15 | public bool SupportsOutbox => true;
16 |
17 | public bool SupportsPurgeOnStartup => true;
18 |
19 | public IConfigureEndpointTestExecution CreateTransportConfiguration() => new ConfigureEndpointRabbitMQTransport();
20 |
21 | public IConfigureEndpointTestExecution CreatePersistenceConfiguration() => new ConfigureEndpointAcceptanceTestingPersistence();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/ChannelExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests
2 | {
3 | using System.Collections.Generic;
4 | using System.Threading.Tasks;
5 | using global::RabbitMQ.Client;
6 |
7 | static class ChannelExtensions
8 | {
9 | public static Task DeclareQuorumQueue(this IChannel channel, string queueName)
10 | {
11 | return channel.QueueDeclareAsync(queueName, true, false, false, new Dictionary
12 | {
13 | { "x-queue-type", "quorum" }
14 | });
15 | }
16 |
17 | public static Task DeclareClassicQueue(this IChannel channel, string queueName)
18 | {
19 | return channel.QueueDeclareAsync(queueName, true, false, false);
20 | }
21 | }
22 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Administration/QueuePurger.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ
2 | {
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 |
6 | class QueuePurger
7 | {
8 | readonly ConnectionFactory connectionFactory;
9 |
10 | public QueuePurger(ConnectionFactory connectionFactory)
11 | {
12 | this.connectionFactory = connectionFactory;
13 | }
14 |
15 | public async Task Purge(string queue, CancellationToken cancellationToken = default)
16 | {
17 | using var connection = await connectionFactory.CreateAdministrationConnection(cancellationToken).ConfigureAwait(false);
18 | using var channel = await connection.CreateChannelAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
19 | await channel.QueuePurgeAsync(queue, cancellationToken).ConfigureAwait(false);
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.CommandLine.Tests/NServiceBus.Transport.RabbitMQ.CommandLine.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net10.0
5 | true
6 | ..\NServiceBusTests.snk
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Configuration/CertificateLoader.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | namespace NServiceBus.Transport.RabbitMQ;
4 |
5 | using System;
6 | using System.Security.Cryptography.X509Certificates;
7 |
8 | static class CertificateLoader
9 | {
10 | public static X509Certificate2 LoadCertificateFromFile(string path, string? password)
11 | {
12 | var contentType = X509Certificate2.GetCertContentType(path);
13 |
14 | #pragma warning disable IDE0072 // Add missing cases
15 | var certificate = contentType switch
16 | {
17 | X509ContentType.Cert => X509CertificateLoader.LoadCertificateFromFile(path),
18 | X509ContentType.Pkcs12 => X509CertificateLoader.LoadPkcs12FromFile(path, password),
19 | _ => throw new NotSupportedException($"Certificate content type '{contentType}' is not supported.")
20 | };
21 | #pragma warning restore IDE0072 // Add missing cases
22 |
23 | return certificate;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.slnx:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Receiving/ModelExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ
2 | {
3 | using System.Threading;
4 | using System.Threading.Tasks;
5 | using global::RabbitMQ.Client;
6 | using global::RabbitMQ.Client.Exceptions;
7 |
8 | static class ModelExtensions
9 | {
10 | public static ValueTask BasicAckSingle(this IChannel channel, ulong deliveryTag, CancellationToken cancellationToken = default) =>
11 | channel.BasicAckAsync(deliveryTag, false, cancellationToken);
12 |
13 | public static async Task BasicRejectAndRequeueIfOpen(this IChannel channel, ulong deliveryTag, CancellationToken cancellationToken = default)
14 | {
15 | if (channel.IsOpen)
16 | {
17 | try
18 | {
19 | await channel.BasicRejectAsync(deliveryTag, true, cancellationToken).ConfigureAwait(false);
20 | }
21 | catch (AlreadyClosedException)
22 | {
23 | }
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.CommandLine/RoutingTopologyBinder.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.CommandLine
2 | {
3 | using System.CommandLine;
4 |
5 | class RoutingTopologyBinder(Option routingTopologyTypeOption, Option useDurableEntitiesOption, Option queueTypeOption)
6 | {
7 | public IRoutingTopology CreateRoutingTopology(ParseResult parseResult)
8 | {
9 | var routingTopologyType = parseResult.GetValue(routingTopologyTypeOption);
10 | var useDurableEntities = parseResult.GetValue(useDurableEntitiesOption);
11 | var queueType = parseResult.GetValue(queueTypeOption);
12 |
13 | return routingTopologyType switch
14 | {
15 | RoutingTopologyType.Conventional => new ConventionalRoutingTopology(useDurableEntities, queueType),
16 | RoutingTopologyType.Direct => new DirectRoutingTopology(useDurableEntities, queueType),
17 | _ => throw new InvalidOperationException()
18 | };
19 | }
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.TransportTests/NServiceBus.Transport.RabbitMQ.TransportTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net10.0
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.Tests/NServiceBus.Transport.RabbitMQ.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net10.0
5 | true
6 | ..\NServiceBusTests.snk
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.TransportTests/When_delayed_delivery_is_disabled.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.TransportTests
2 | {
3 | using System;
4 | using System.Threading.Tasks;
5 | using DelayedDelivery;
6 | using NServiceBus.TransportTests;
7 | using NUnit.Framework;
8 |
9 | public class When_delayed_delivery_is_disabled : NServiceBusTransportTest
10 | {
11 | [TestCase(TransportTransactionMode.ReceiveOnly)]
12 | public async Task Should_throw_when_sending(TransportTransactionMode transactionMode)
13 | {
14 | await StartPump(
15 | (context, _) => Task.FromResult(0),
16 | (_, __) => Task.FromResult(ErrorHandleResult.RetryRequired),
17 | transactionMode);
18 |
19 | Assert.ThrowsAsync(async () =>
20 | {
21 | var dispatchProperties = new DispatchProperties { DelayDeliveryWith = new DelayDeliveryWith(TimeSpan.FromHours(1)) };
22 | await SendMessage(InputQueueName, [], null, dispatchProperties);
23 | });
24 | }
25 | }
26 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: Feature request
2 |
3 | description: Request a new feature.
4 | labels: ["Feature" ]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | Please check all open and closed issues to see if the feature has already been requested.
10 |
11 | - type: textarea
12 | id: description
13 | attributes:
14 | label: Describe the feature.
15 |
16 | description: A clear and concise description of the feature.
17 |
18 | value: |
19 | #### Is your feature related to a problem? Please describe.
20 |
21 | #### Describe the requested feature
22 |
23 | #### Describe alternatives you've considered
24 | validations:
25 | required: true
26 | - type: textarea
27 | id: additional-context
28 | attributes:
29 | label: Additional Context
30 | description: Add any other context about the request.
31 |
32 | placeholder: "Add screenshots or additional information."
33 | validations:
34 | required: false
35 | - type: markdown
36 | attributes:
37 | value: |
38 | For additional support options visit [particular.net/support](https://particular.net/support)
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/improvement_request.yml:
--------------------------------------------------------------------------------
1 | name: Improvement request
2 | description: Suggest an improvement to an existing code base.
3 | labels: ["Improvement" ]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | Please check all open and closed issues to see if improvement has already been suggested.
9 | - type: textarea
10 | id: description
11 | attributes:
12 | label: Describe the suggested improvement
13 | description: A clear and concise description of what the improvement is.
14 | value: |
15 | #### Is your improvement related to a problem? Please describe.
16 |
17 | #### Describe the suggested solution
18 |
19 | #### Describe alternatives you've considered
20 | validations:
21 | required: true
22 | - type: textarea
23 | id: additional-context
24 | attributes:
25 | label: Additional Context
26 | description: Add any other context about the suggestion here.
27 | placeholder: "Add screenshots or additional information."
28 | validations:
29 | required: false
30 | - type: markdown
31 | attributes:
32 | value: |
33 | For additional support options visit [particular.net/support](https://particular.net/support)
34 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Administration/ManagementApi/Converters/DeliveryLimitConverter.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | namespace NServiceBus.Transport.RabbitMQ.ManagementApi;
4 |
5 | using System;
6 | using System.Text.Json;
7 | using System.Text.Json.Serialization;
8 |
9 | class DeliveryLimitConverter : JsonConverter
10 | {
11 | public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
12 | {
13 | if (reader.TokenType == JsonTokenType.Number)
14 | {
15 | return reader.GetInt32();
16 | }
17 | else if (reader.TokenType == JsonTokenType.String)
18 | {
19 | var value = reader.GetString();
20 |
21 | if (string.Equals(value, "unlimited", StringComparison.OrdinalIgnoreCase))
22 | {
23 | return -1;
24 | }
25 |
26 | throw new JsonException($"Unexpected string value for delivery limit: {value}");
27 | }
28 | else
29 | {
30 | throw new JsonException($"Expected delivery limit to be either a number or the string 'unlimited', not a '{reader.TokenType}'");
31 | }
32 | }
33 |
34 | public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options) => writer.WriteNumberValue(value);
35 | }
36 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/NServiceBus.Transport.RabbitMQ.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net10.0
5 | true
6 | ..\NServiceBus.snk
7 | true
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | NServiceBus.RabbitMQ
23 | RabbitMQ support for NServiceBus
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.CommandLine/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "profiles": {
3 | "NServiceBus.RabbitMQ.CommandLine": {
4 | "commandName": "Project"
5 | },
6 | "delays migrate": {
7 | "commandName": "Executable",
8 | "executablePath": ".\\NServiceBus.Transport.RabbitMQ.CommandLine.exe",
9 | "commandLineArgs": "delays migrate"
10 | },
11 | "delays create": {
12 | "commandName": "Executable",
13 | "executablePath": ".\\NServiceBus.Transport.RabbitMQ.CommandLine.exe",
14 | "commandLineArgs": "delays create"
15 | },
16 | "delays verify": {
17 | "commandName": "Executable",
18 | "executablePath": ".\\NServiceBus.Transport.RabbitMQ.CommandLine.exe",
19 | "commandLineArgs": "delays verify"
20 | },
21 | "endpoint create": {
22 | "commandName": "Executable",
23 | "executablePath": ".\\NServiceBus.Transport.RabbitMQ.CommandLine.exe",
24 | "commandLineArgs": "endpoint create TestEndpoint"
25 | },
26 | "queue migrate": {
27 | "commandName": "Executable",
28 | "executablePath": ".\\NServiceBus.Transport.RabbitMQ.CommandLine.exe",
29 | "commandLineArgs": "queue migrate-to-quorum TestEndpoint"
30 | },
31 | "queue validate delivery limit": {
32 | "commandName": "Executable",
33 | "executablePath": ".\\NServiceBus.Transport.RabbitMQ.CommandLine.exe",
34 | "commandLineArgs": "queue validate-delivery-limit TestEndpoint"
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/NServiceBus.Transport.RabbitMQ.AcceptanceTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net10.0
5 | true
6 | ..\NServiceBusTests.snk
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Administration/ManagementApi/Converters/QueueTypeConverter.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | namespace NServiceBus.Transport.RabbitMQ.ManagementApi;
4 |
5 | using System;
6 | using System.Text.Json;
7 | using System.Text.Json.Serialization;
8 |
9 | class QueueTypeConverter : JsonConverter
10 | {
11 | public override QueueType Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
12 | {
13 | var value = reader.GetString();
14 |
15 | return value switch
16 | {
17 | "quorum" => QueueType.Quorum,
18 | "classic" => QueueType.Classic,
19 | "stream" => QueueType.Stream,
20 | "rabbit_mqtt_qos0_queue" => QueueType.MqttQos0,
21 | _ => QueueType.Unknown
22 | };
23 | }
24 |
25 | public override void Write(Utf8JsonWriter writer, QueueType queueType, JsonSerializerOptions options)
26 | {
27 | var value = queueType switch
28 | {
29 | QueueType.Quorum => "quorum",
30 | QueueType.Classic => "classic",
31 | QueueType.Stream => "stream",
32 | QueueType.MqttQos0 => "rabbit_mqtt_qos0_queue",
33 | QueueType.Unknown => throw new ArgumentOutOfRangeException(nameof(queueType), "Cannot write unknown queue type"),
34 | _ => throw new ArgumentOutOfRangeException(nameof(queueType), $"QueueType value out of range: {queueType}")
35 | };
36 |
37 | writer.WriteStringValue(value);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 | on:
3 | push:
4 | tags:
5 | - '[0-9]+.[0-9]+.[0-9]+'
6 | - '[0-9]+.[0-9]+.[0-9]+-*'
7 | env:
8 | DOTNET_NOLOGO: true
9 | defaults:
10 | run:
11 | shell: pwsh
12 | jobs:
13 | release:
14 | runs-on: ubuntu-latest
15 | steps:
16 | - name: Checkout
17 | uses: actions/checkout@v6.0.1
18 | with:
19 | fetch-depth: 0
20 | - name: Setup .NET SDK
21 | uses: actions/setup-dotnet@v5.0.1
22 | with:
23 | global-json-file: global.json
24 | - name: Build
25 | run: dotnet build src --configuration Release
26 | - name: Sign NuGet packages
27 | uses: Particular/sign-nuget-packages-action@v1.0.0
28 | with:
29 | client-id: ${{ secrets.AZURE_KEY_VAULT_CLIENT_ID }}
30 | tenant-id: ${{ secrets.AZURE_KEY_VAULT_TENANT_ID }}
31 | client-secret: ${{ secrets.AZURE_KEY_VAULT_CLIENT_SECRET }}
32 | certificate-name: ${{ secrets.AZURE_KEY_VAULT_CERTIFICATE_NAME }}
33 | - name: Publish artifacts
34 | uses: actions/upload-artifact@v6.0.0
35 | with:
36 | name: nugets
37 | path: nugets/*
38 | retention-days: 1
39 | - name: Deploy
40 | # Does not follow standard practice of targeting explicit versions because configuration is tightly coupled to Octopus Deploy configuration
41 | uses: Particular/push-octopus-package-action@main
42 | with:
43 | octopus-deploy-api-key: ${{ secrets.OCTOPUS_DEPLOY_API_KEY }}
44 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.TransportTests/ConnectionHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Security.Authentication;
3 | using NServiceBus.Transport.RabbitMQ;
4 | using RabbitMQ.Client;
5 |
6 | public class ConnectionHelper
7 | {
8 | static Lazy connectionString = new Lazy(() =>
9 | {
10 | var connectionString = Environment.GetEnvironmentVariable("RabbitMQTransport_ConnectionString") ?? "host=localhost";
11 |
12 | return connectionString;
13 | });
14 |
15 | static Lazy connectionFactory = new Lazy(() =>
16 | {
17 | var connectionStringParser = ConnectionConfiguration.Create(ConnectionString);
18 |
19 | var factory = new ConnectionFactory
20 | {
21 | AutomaticRecoveryEnabled = true,
22 | HostName = connectionStringParser.Host,
23 | Port = connectionStringParser.Port,
24 | VirtualHost = connectionStringParser.VirtualHost,
25 | UserName = connectionStringParser.UserName ?? "guest",
26 | Password = connectionStringParser.Password ?? "guest"
27 | };
28 |
29 | factory.Ssl.ServerName = factory.HostName;
30 | factory.Ssl.Certs = null;
31 | factory.Ssl.Version = SslProtocols.Tls12;
32 | factory.Ssl.Enabled = connectionStringParser.UseTls;
33 |
34 | return factory;
35 | });
36 |
37 | public static string ConnectionString => connectionString.Value;
38 |
39 | public static ConnectionFactory ConnectionFactory => connectionFactory.Value;
40 | }
41 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.Tests/Support/ConventionalRoutingTopologyExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.Tests.Support
2 | {
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 |
8 | static class ConventionalRoutingTopologyExtensions
9 | {
10 | public static async Task ResetAsync(
11 | this ConventionalRoutingTopology routingTopology,
12 | ConnectionFactory connectionFactory,
13 | IEnumerable receivingAddresses,
14 | IEnumerable sendingAddresses,
15 | CancellationToken cancellationToken = default)
16 | {
17 | using var connection = await connectionFactory.CreateAdministrationConnection(cancellationToken);
18 | using var channel = await connection.CreateChannelAsync(cancellationToken: cancellationToken);
19 | foreach (var address in receivingAddresses.Concat(sendingAddresses))
20 | {
21 | await channel.QueueDeleteAsync(address, false, false, cancellationToken: cancellationToken);
22 | await channel.ExchangeDeleteAsync(address, false, cancellationToken: cancellationToken);
23 | }
24 |
25 | await DelayInfrastructure.TearDown(channel, cancellationToken);
26 | await DelayInfrastructure.Build(channel, cancellationToken);
27 |
28 | await routingTopology.Initialize(channel, receivingAddresses, sendingAddresses, cancellationToken);
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.CommandLine/Commands/Delays/DelaysVerifyCommand.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.CommandLine
2 | {
3 | using System;
4 | using System.CommandLine;
5 |
6 | class DelaysVerifyCommand(BrokerVerifier brokerVerifier, TextWriter output)
7 | {
8 | public static Command CreateCommand()
9 | {
10 | var command = new Command("verify", "Verify broker requirements for using the v2 delay infrastructure");
11 |
12 | var brokerVerifierBinder = SharedOptions.CreateBrokerVerifierBinderWithOptions(command);
13 |
14 | command.SetAction(async (parseResult, cancellationToken) =>
15 | {
16 | var brokerVerifier = brokerVerifierBinder.CreateBrokerVerifier(parseResult);
17 |
18 | var delaysVerify = new DelaysVerifyCommand(brokerVerifier, parseResult.InvocationConfiguration.Output);
19 | await delaysVerify.Run(cancellationToken);
20 | });
21 |
22 | return command;
23 | }
24 |
25 | public async Task Run(CancellationToken cancellationToken = default)
26 | {
27 | try
28 | {
29 | await brokerVerifier.Initialize(cancellationToken);
30 | await brokerVerifier.VerifyRequirements(cancellationToken);
31 |
32 | output.WriteLine("All checks OK");
33 | }
34 | catch (Exception ex) when (!ex.IsCausedBy(cancellationToken))
35 | {
36 | output.WriteLine($"Fail: {ex.Message}");
37 | }
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Sending/NonPersistentDeliveryModeExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus
2 | {
3 | using System;
4 | using Transport.RabbitMQ;
5 |
6 | ///
7 | /// Adds extension methods for the relevant ExtendableOptions classes.
8 | ///
9 | public static class NonPersistentDeliveryModeExtensions
10 | {
11 | ///
12 | /// Uses the non-persistent delivery mode to send the message.
13 | ///
14 | public static void UseNonPersistentDeliveryMode(this SendOptions options)
15 | {
16 | ArgumentNullException.ThrowIfNull(options);
17 |
18 | options.SetHeader(BasicPropertiesExtensions.UseNonPersistentDeliveryHeader, bool.TrueString);
19 | }
20 |
21 | ///
22 | /// Uses the non-persistent delivery mode to publish the message.
23 | ///
24 | public static void UseNonPersistentDeliveryMode(this PublishOptions options)
25 | {
26 | ArgumentNullException.ThrowIfNull(options);
27 |
28 | options.SetHeader(BasicPropertiesExtensions.UseNonPersistentDeliveryHeader, bool.TrueString);
29 | }
30 |
31 | ///
32 | /// Uses the non-persistent delivery mode to send the reply.
33 | ///
34 | public static void UseNonPersistentDeliveryMode(this ReplyOptions options)
35 | {
36 | ArgumentNullException.ThrowIfNull(options);
37 |
38 | options.SetHeader(BasicPropertiesExtensions.UseNonPersistentDeliveryHeader, bool.TrueString);
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.CommandLine/Commands/Delays/DelaysCreateCommand.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.CommandLine
2 | {
3 | using System.CommandLine;
4 | using System.Threading;
5 |
6 | class DelaysCreateCommand(BrokerConnection brokerConnection, TextWriter output)
7 | {
8 | public static Command CreateCommand()
9 | {
10 | var command = new Command("create", "Create v2 delay infrastructure queues and exchanges");
11 |
12 | var brokerConnectionBinder = SharedOptions.CreateBrokerConnectionBinderWithOptions(command);
13 |
14 | command.SetAction(async (parseResult, cancellationToken) =>
15 | {
16 | var brokerConnection = brokerConnectionBinder.CreateBrokerConnection(parseResult);
17 |
18 | var delaysCreate = new DelaysCreateCommand(brokerConnection, parseResult.InvocationConfiguration.Output);
19 | await delaysCreate.Run(cancellationToken);
20 | });
21 |
22 | return command;
23 | }
24 |
25 | public async Task Run(CancellationToken cancellationToken = default)
26 | {
27 | output.WriteLine($"Creating v2 delay infrastructure queues and exchanges...");
28 |
29 | using var connection = await brokerConnection.Create(cancellationToken);
30 | using var channel = await connection.CreateChannelAsync(cancellationToken: cancellationToken);
31 |
32 | await DelayInfrastructure.Build(channel, cancellationToken);
33 |
34 | output.WriteLine("Queues and exchanges created successfully");
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.CommandLine/Program.cs:
--------------------------------------------------------------------------------
1 | using System.CommandLine;
2 | using NServiceBus.Transport.RabbitMQ.CommandLine;
3 |
4 | var rootCommand = new RootCommand("A tool to manage the RabbitMQ transport for NServiceBus endpoints");
5 |
6 | CreateDelaysCommand(rootCommand);
7 | CreateEndpointCommand(rootCommand);
8 | CreateQueueCommand(rootCommand);
9 |
10 | var parseResult = rootCommand.Parse(args);
11 |
12 | return await parseResult.InvokeAsync();
13 |
14 | void CreateDelaysCommand(Command rootCommand)
15 | {
16 | var delaysSubCommand = new Command("delays", "Commands to manage the queues and exchanges for the NServiceBus delay infrastructure");
17 |
18 | delaysSubCommand.Subcommands.Add(DelaysCreateCommand.CreateCommand());
19 | delaysSubCommand.Subcommands.Add(DelaysMigrateCommand.CreateCommand());
20 | delaysSubCommand.Subcommands.Add(DelaysVerifyCommand.CreateCommand());
21 |
22 | rootCommand.Subcommands.Add(delaysSubCommand);
23 | }
24 |
25 | void CreateEndpointCommand(Command rootCommand)
26 | {
27 | var endpointSubCommand = new Command("endpoint", "Commands to manage the infrastructure for an NServiceBus endpoint");
28 |
29 | endpointSubCommand.Subcommands.Add(EndpointCreateCommand.CreateCommand());
30 |
31 | rootCommand.Subcommands.Add(endpointSubCommand);
32 | }
33 |
34 | void CreateQueueCommand(Command rootCommand)
35 | {
36 | var queueSubCommand = new Command("queue", "Commands to manage individual queues");
37 |
38 | queueSubCommand.Subcommands.Add(QueueMigrateCommand.CreateCommand());
39 | queueSubCommand.Subcommands.Add(QueueValidateDeliveryLimitCommand.CreateCommand());
40 |
41 | rootCommand.Subcommands.Add(queueSubCommand);
42 | }
43 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Administration/ManagementApi/Converters/PolicyTargetConverter.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | namespace NServiceBus.Transport.RabbitMQ.ManagementApi;
4 |
5 | using System;
6 | using System.Text.Json;
7 | using System.Text.Json.Serialization;
8 |
9 | class PolicyTargetConverter : JsonConverter
10 | {
11 | public override PolicyTarget Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
12 | {
13 | var value = reader.GetString();
14 |
15 | return value switch
16 | {
17 | "all" => PolicyTarget.All,
18 | "queues" => PolicyTarget.Queues,
19 | "classic_queues" => PolicyTarget.ClassicQueues,
20 | "quorum_queues" => PolicyTarget.QuorumQueues,
21 | "streams" => PolicyTarget.Streams,
22 | "exchanges" => PolicyTarget.Exchanges,
23 | _ => throw new JsonException($"Unknown policy target: {value}")
24 | };
25 | }
26 |
27 | public override void Write(Utf8JsonWriter writer, PolicyTarget policyTarget, JsonSerializerOptions options)
28 | {
29 | var value = policyTarget switch
30 | {
31 | PolicyTarget.All => "all",
32 | PolicyTarget.Queues => "queues",
33 | PolicyTarget.ClassicQueues => "classic_queues",
34 | PolicyTarget.QuorumQueues => "quorum_queues",
35 | PolicyTarget.Streams => "streams",
36 | PolicyTarget.Exchanges => "exchanges",
37 | _ => throw new ArgumentOutOfRangeException(nameof(policyTarget), $"PolicyTarget value out of range: {policyTarget}")
38 | };
39 |
40 | writer.WriteStringValue(value);
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/ConnectionHelper.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests
2 | {
3 | using System;
4 | using System.Security.Authentication;
5 | using global::RabbitMQ.Client;
6 |
7 | public class ConnectionHelper
8 | {
9 | static Lazy connectionString = new Lazy(() =>
10 | {
11 | var connectionString = Environment.GetEnvironmentVariable("RabbitMQTransport_ConnectionString") ?? "host=localhost";
12 |
13 | return connectionString;
14 | });
15 |
16 | static Lazy connectionFactory = new Lazy(() =>
17 | {
18 | var connectionConfiguration = ConnectionConfiguration.Create(ConnectionString);
19 |
20 | var factory = new ConnectionFactory
21 | {
22 | AutomaticRecoveryEnabled = true,
23 | HostName = connectionConfiguration.Host,
24 | Port = connectionConfiguration.Port,
25 | VirtualHost = connectionConfiguration.VirtualHost,
26 | UserName = connectionConfiguration.UserName ?? "guest",
27 | Password = connectionConfiguration.Password ?? "guest"
28 | };
29 |
30 | factory.Ssl.ServerName = factory.HostName;
31 | factory.Ssl.Certs = null;
32 | factory.Ssl.Version = SslProtocols.Tls12;
33 | factory.Ssl.Enabled = connectionConfiguration.UseTls;
34 |
35 | return factory;
36 | });
37 |
38 | public static string ConnectionString => connectionString.Value;
39 |
40 | public static ConnectionFactory ConnectionFactory => connectionFactory.Value;
41 | }
42 | }
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yml:
--------------------------------------------------------------------------------
1 | name: Bug report
2 | description: File a bug report
3 | labels: ["Bug" ]
4 | body:
5 | - type: markdown
6 | attributes:
7 | value: |
8 | For additional support options, or if this is related to a security vulnerability or sensitive information must be included in the bug report, visit [particular.net/support](https://particular.net/support).
9 | - type: textarea
10 | id: what-happened
11 | attributes:
12 | label: Describe the bug
13 | description: A clear and concise description of the bug.
14 | value: |
15 | #### Description
16 |
17 | #### Expected behavior
18 |
19 | #### Actual behavior
20 |
21 | #### Versions
22 |
23 | Please list the version of the relevant packages or applications in which the bug exists.
24 | validations:
25 | required: true
26 | - type: textarea
27 | id: steps-to-reproduce
28 | attributes:
29 | label: Steps to reproduce
30 | description: Detailed instructions to reproduce the bug.
31 | validations:
32 | required: true
33 | - type: textarea
34 | id: logs
35 | attributes:
36 | label: Relevant log output
37 | description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks.
38 | render: shell
39 | - type: textarea
40 | id: additional-information
41 | attributes:
42 | label: Additional Information
43 | description: If there are any possible solutions, workarounds, or additional information please describe them here
44 | value: |
45 | #### Workarounds
46 |
47 | #### Possible solutions
48 |
49 | #### Additional information
50 | validations:
51 | required: false
52 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/QuorumQueues/When_quorum_endpoint_uses_classic_queue.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests
2 | {
3 | using System;
4 | using System.Threading.Tasks;
5 | using AcceptanceTesting;
6 | using AcceptanceTesting.Customization;
7 | using NServiceBus.AcceptanceTests;
8 | using NUnit.Framework;
9 |
10 | public class When_quorum_endpoint_uses_classic_queue : NServiceBusAcceptanceTest
11 | {
12 | [Test]
13 | public async Task Should_fail_to_start()
14 | {
15 | using (var connection = await ConnectionHelper.ConnectionFactory.CreateConnectionAsync())
16 | using (var channel = await connection.CreateChannelAsync())
17 | {
18 | await channel.DeclareClassicQueue(Conventions.EndpointNamingConvention(typeof(QuorumQueueEndpoint)));
19 | }
20 |
21 | var exception = Assert.CatchAsync(async () => await Scenario.Define()
22 | .WithEndpoint()
23 | .Done(c => c.EndpointsStarted)
24 | .Run());
25 |
26 | Assert.That(exception.Message, Does.Contain("PRECONDITION_FAILED - inequivalent arg 'x-queue-type' for queue 'QuorumEndpointUsesClassicQueue.QuorumQueueEndpoint'"));
27 | Assert.That(exception.Message, Does.Contain("received the value 'quorum' of type 'longstr' but current is none'") // RabbitMQ v3.x
28 | .Or.Contain("received 'quorum' but current is 'classic'")); // RabbitMQ v4.x
29 | }
30 |
31 | class QuorumQueueEndpoint : EndpointConfigurationBuilder
32 | {
33 | public QuorumQueueEndpoint()
34 | {
35 | EndpointSetup();
36 | }
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.CommandLine/Commands/Queue/QueueValidateDeliveryLimitCommand.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.CommandLine
2 | {
3 | using System.CommandLine;
4 |
5 | class QueueValidateDeliveryLimitCommand(string queueName, BrokerVerifier brokerVerifier, TextWriter output)
6 | {
7 | public static Command CreateCommand()
8 | {
9 | var command = new Command("validate-delivery-limit", "Validate that the queue has an unlimited delivery limit");
10 |
11 | var queueNameArgument = new Argument("queueName")
12 | {
13 | Description = "The name of the queue to validate"
14 | };
15 |
16 | var brokerVerifierBinder = SharedOptions.CreateBrokerVerifierBinderWithOptions(command);
17 |
18 | command.Arguments.Add(queueNameArgument);
19 |
20 | command.SetAction(async (parseResult, cancellationToken) =>
21 | {
22 | var brokerVerifier = brokerVerifierBinder.CreateBrokerVerifier(parseResult);
23 |
24 | var validateCommand = new QueueValidateDeliveryLimitCommand(parseResult.GetRequiredValue(queueNameArgument), brokerVerifier, parseResult.InvocationConfiguration.Output);
25 | await validateCommand.Run(cancellationToken);
26 | });
27 |
28 | return command;
29 | }
30 |
31 | public async Task Run(CancellationToken cancellationToken = default)
32 | {
33 | try
34 | {
35 | await brokerVerifier.Initialize(cancellationToken);
36 | await brokerVerifier.ValidateDeliveryLimit(queueName, cancellationToken);
37 | }
38 | catch (Exception ex) when (!ex.IsCausedBy(cancellationToken))
39 | {
40 | output.WriteLine(ex.Message);
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /assets
2 | /binaries
3 | /deploy
4 | /nugets
5 | build32
6 | *.vshost.*
7 | .nu
8 | _UpgradeReport.*
9 | *.cache
10 | Thumbs.db
11 | *~
12 | *.swp
13 | results
14 | CommonAssemblyInfo.cs
15 | lib/sqlite/System.Data.SQLite.dll
16 | *.orig
17 | *.zip
18 | Samples/DataBus/storage
19 | packages
20 | PrecompiledWeb
21 | tempstorage
22 | .learningtransport
23 | core-only
24 | Release
25 | Artifacts
26 | LogFiles
27 | csx
28 | *.ncrunchproject
29 | *.ncrunchsolution
30 | _NCrunch_NServiceBus/*
31 | logs
32 | run-git.cmd
33 | src/Chocolatey/Build/*
34 |
35 | installer/[F|f]iles
36 | installer/[C|c]ustom[A|a]ctions
37 | installer/ServiceControl-cache
38 |
39 | # Created by https://www.gitignore.io
40 |
41 | ### VisualStudio ###
42 | ## Ignore Visual Studio temporary files, build results, and
43 | ## files generated by popular Visual Studio add-ons.
44 |
45 | # User-specific files
46 | *.suo
47 | *.user
48 | *.json.lock
49 | *.nuget.targets
50 | *.lock.json
51 | *.userosscache
52 | *.sln.docstates
53 | .vs/
54 | local.settings.json
55 |
56 | # mac temp file ignore
57 | .DS_Store
58 |
59 | # Build results
60 | [Dd]ebug/
61 | [Dd]ebugPublic/
62 | [Rr]elease/
63 | [Rr]eleases/
64 | x64/
65 | x86/
66 | build/
67 | bld/
68 | [Bb]in/
69 | [Oo]bj/
70 | *.binlog
71 |
72 | # Roslyn cache directories
73 | *.ide/
74 |
75 | # MSTest test Results
76 | [Tt]est[Rr]esult*/
77 | [Bb]uild[Ll]og.*
78 |
79 | #NUNIT
80 | *.VisualState.xml
81 | TestResult.xml
82 |
83 | # NCrunch
84 | _NCrunch_*
85 | .*crunch*.local.xml
86 |
87 | # ReSharper is a .NET coding add-in
88 | _ReSharper*/
89 | *.[Rr]e[Ss]harper
90 | *.DotSettings
91 | *.DotSettings.user
92 |
93 | src/scaffolding.config
94 |
95 | # Approval tests temp file
96 | *.received.*
97 |
98 | # JetBrains Rider
99 | .idea/
100 | *.sln.iml
101 |
102 | # Visual Studio Code
103 | .vscode
104 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/QuorumQueues/When_classic_endpoint_uses_quorum_queue.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests
2 | {
3 | using System;
4 | using System.Threading.Tasks;
5 | using AcceptanceTesting;
6 | using AcceptanceTesting.Customization;
7 | using NServiceBus.AcceptanceTests;
8 | using NServiceBus.AcceptanceTests.EndpointTemplates;
9 | using NUnit.Framework;
10 |
11 | public class When_classic_endpoint_uses_quorum_queue : NServiceBusAcceptanceTest
12 | {
13 | [Test]
14 | public async Task Should_fail_to_start()
15 | {
16 | using (var connection = await ConnectionHelper.ConnectionFactory.CreateConnectionAsync())
17 | using (var channel = await connection.CreateChannelAsync())
18 | {
19 | await channel.DeclareQuorumQueue(Conventions.EndpointNamingConvention(typeof(ClassicQueueEndpoint)));
20 | }
21 |
22 | var exception = Assert.CatchAsync(async () => await Scenario.Define()
23 | .WithEndpoint()
24 | .Done(c => c.EndpointsStarted)
25 | .Run());
26 |
27 | Assert.That(exception.Message, Does.Contain("PRECONDITION_FAILED - inequivalent arg 'x-queue-type' for queue 'ClassicEndpointUsesQuorumQueue.ClassicQueueEndpoint'"));
28 | Assert.That(exception.Message, Does.Contain("received none but current is the value 'quorum'") // RabbitMQ v3.x
29 | .Or.Contain("received 'classic' but current is 'quorum'")); // RabbitMQ v4.x
30 | }
31 |
32 | class ClassicQueueEndpoint : EndpointConfigurationBuilder
33 | {
34 | public ClassicQueueEndpoint()
35 | {
36 | EndpointSetup();
37 | }
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/DelayedDelivery/When_deferring_a_message_longer_than_allowed_maximum.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests.DelayedDelivery
2 | {
3 | using System;
4 | using System.Threading.Tasks;
5 | using AcceptanceTesting;
6 | using NServiceBus.AcceptanceTests;
7 | using NServiceBus.AcceptanceTests.EndpointTemplates;
8 | using NUnit.Framework;
9 |
10 | public class When_deferring_a_message_longer_than_allowed_maximum : NServiceBusAcceptanceTest
11 | {
12 | [Test]
13 | public void Should_throw()
14 | {
15 | var delay = TimeSpan.FromDays(365 * 1000);
16 |
17 | var exception = Assert.ThrowsAsync(() => Scenario.Define()
18 | .WithEndpoint(b => b.When((session, c) =>
19 | {
20 | var options = new SendOptions();
21 |
22 | options.DelayDeliveryWith(delay);
23 | options.RouteToThisEndpoint();
24 |
25 | return session.Send(new MyMessage(), options);
26 | }))
27 | .Done(context => !context.FailedMessages.IsEmpty)
28 | .Run());
29 |
30 | Assert.That(exception, Is.Not.Null);
31 | Assert.That(exception.Message, Does.StartWith("Message cannot be delayed by"));
32 | }
33 |
34 | public class Endpoint : EndpointConfigurationBuilder
35 | {
36 | public Endpoint()
37 | {
38 | EndpointSetup();
39 | }
40 |
41 | public class MyMessageHandler : IHandleMessages
42 | {
43 | public Task Handle(MyMessage message, IMessageHandlerContext context) => Task.CompletedTask;
44 | }
45 | }
46 |
47 | public class MyMessage : IMessage { }
48 | }
49 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/QuorumQueues/When_classic_endpoint_uses_quorum_error_queue.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests
2 | {
3 | using System;
4 | using System.Threading.Tasks;
5 | using AcceptanceTesting;
6 | using NServiceBus.AcceptanceTests;
7 | using NServiceBus.AcceptanceTests.EndpointTemplates;
8 | using NUnit.Framework;
9 |
10 | public class When_classic_endpoint_uses_quorum_error_queue : NServiceBusAcceptanceTest
11 | {
12 | [Test]
13 | public async Task Should_fail_to_start()
14 | {
15 | using (var connection = await ConnectionHelper.ConnectionFactory.CreateConnectionAsync())
16 | using (var channel = await connection.CreateChannelAsync())
17 | {
18 | await channel.DeclareQuorumQueue("rabbitmq.transport.tests.quorum-error");
19 | }
20 |
21 | var exception = Assert.CatchAsync(async () => await Scenario.Define()
22 | .WithEndpoint()
23 | .Done(c => c.EndpointsStarted)
24 | .Run());
25 |
26 | Assert.That(exception.Message, Does.Contain("PRECONDITION_FAILED - inequivalent arg 'x-queue-type' for queue 'rabbitmq.transport.tests.quorum-error'"));
27 | Assert.That(exception.Message, Does.Contain("received none but current is the value 'quorum'") // RabbitMQ v3.x
28 | .Or.Contain("received 'classic' but current is 'quorum'")); // RabbitMQ v4.x
29 | }
30 |
31 | class ClassicQueueEndpoint : EndpointConfigurationBuilder
32 | {
33 | public ClassicQueueEndpoint()
34 | {
35 | EndpointSetup(c => c
36 | .SendFailedMessagesTo("rabbitmq.transport.tests.quorum-error"));
37 | }
38 | }
39 | }
40 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/QuorumQueues/When_quorum_endpoint_uses_classic_error_queue.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests
2 | {
3 | using System;
4 | using System.Threading.Tasks;
5 | using AcceptanceTesting;
6 | using NServiceBus.AcceptanceTests;
7 | using NUnit.Framework;
8 |
9 | public class When_quorum_endpoint_uses_classic_error_queue : NServiceBusAcceptanceTest
10 | {
11 | [Test]
12 | public async Task Should_fail_to_start()
13 | {
14 | using (var connection = await ConnectionHelper.ConnectionFactory.CreateConnectionAsync())
15 | using (var channel = await connection.CreateChannelAsync())
16 | {
17 | await channel.DeclareClassicQueue("rabbitmq.transport.tests.classic-error");
18 | }
19 |
20 |
21 | var exception = Assert.CatchAsync(async () => await Scenario.Define()
22 | .WithEndpoint()
23 | .Done(c => c.EndpointsStarted)
24 | .Run());
25 |
26 | Assert.That(exception.Message, Does.Contain("PRECONDITION_FAILED - inequivalent arg 'x-queue-type' for queue 'rabbitmq.transport.tests.classic-error'"));
27 | Assert.That(exception.Message, Does.Contain("received the value 'quorum' of type 'longstr' but current is none'") // RabbitMQ v3.x
28 | .Or.Contain("received 'quorum' but current is 'classic'")); // RabbitMQ v4.x
29 | }
30 |
31 | class QuorumQueueEndpoint : EndpointConfigurationBuilder
32 | {
33 | public QuorumQueueEndpoint()
34 | {
35 | EndpointSetup(config =>
36 | {
37 | config.SendFailedMessagesTo("rabbitmq.transport.tests.classic-error");
38 | });
39 | }
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Administration/SubscriptionManager.cs:
--------------------------------------------------------------------------------
1 |
2 | namespace NServiceBus.Transport.RabbitMQ
3 | {
4 | using System.Threading;
5 | using Unicast.Messages;
6 | using System.Threading.Tasks;
7 | using Extensibility;
8 |
9 | class SubscriptionManager : ISubscriptionManager
10 | {
11 | readonly ConnectionFactory connectionFactory;
12 | readonly IRoutingTopology routingTopology;
13 | readonly string localQueue;
14 |
15 | public SubscriptionManager(ConnectionFactory connectionFactory, IRoutingTopology routingTopology, string localQueue)
16 | {
17 | this.connectionFactory = connectionFactory;
18 | this.routingTopology = routingTopology;
19 | this.localQueue = localQueue;
20 | }
21 |
22 | public async Task SubscribeAll(MessageMetadata[] eventTypes, ContextBag context, CancellationToken cancellationToken = default)
23 | {
24 | using var connection = await connectionFactory.CreateAdministrationConnection(cancellationToken).ConfigureAwait(false);
25 | using var channel = await connection.CreateChannelAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
26 | foreach (var eventType in eventTypes)
27 | {
28 | await routingTopology.SetupSubscription(channel, eventType, localQueue, cancellationToken).ConfigureAwait(false);
29 | }
30 | }
31 |
32 | public async Task Unsubscribe(MessageMetadata eventType, ContextBag context, CancellationToken cancellationToken = default)
33 | {
34 | using var connection = await connectionFactory.CreateAdministrationConnection(cancellationToken).ConfigureAwait(false);
35 | using var channel = await connection.CreateChannelAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
36 | await routingTopology.TeardownSubscription(channel, eventType, localQueue, cancellationToken).ConfigureAwait(false);
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - master
6 | - release-*
7 | pull_request:
8 | workflow_dispatch:
9 | env:
10 | DOTNET_NOLOGO: true
11 | defaults:
12 | run:
13 | shell: pwsh
14 | jobs:
15 | build:
16 | name: ${{ matrix.name }}
17 | runs-on: ${{ matrix.os }}
18 | strategy:
19 | matrix:
20 | include:
21 | - os: windows-latest
22 | name: Windows
23 | - os: ubuntu-latest
24 | name: Linux
25 | fail-fast: false
26 | steps:
27 | - name: Check for secrets
28 | env:
29 | SECRETS_AVAILABLE: ${{ secrets.SECRETS_AVAILABLE }}
30 | run: exit $(If ($env:SECRETS_AVAILABLE -eq 'true') { 0 } Else { 1 })
31 | - name: Checkout
32 | uses: actions/checkout@v6.0.1
33 | with:
34 | fetch-depth: 0
35 | - name: Setup .NET SDK
36 | uses: actions/setup-dotnet@v5.0.1
37 | with:
38 | global-json-file: global.json
39 | - name: Build
40 | run: dotnet build src --configuration Release
41 | - name: Upload packages
42 | if: matrix.name == 'Windows'
43 | uses: actions/upload-artifact@v6.0.0
44 | with:
45 | name: NuGet packages
46 | path: nugets/
47 | retention-days: 7
48 | - name: Azure login
49 | uses: azure/login@v2.3.0
50 | with:
51 | creds: ${{ secrets.AZURE_ACI_CREDENTIALS }}
52 | enable-AzPSSession: true
53 | - name: Setup RabbitMQ
54 | uses: Particular/setup-rabbitmq-action@v1.7.1
55 | with:
56 | connection-string-name: RabbitMQTransport_ConnectionString
57 | tag: RabbitMQTransport
58 | image-tag: 4-management
59 | registry-username: ${{ secrets.DOCKERHUB_USERNAME }}
60 | registry-password: ${{ secrets.DOCKERHUB_TOKEN }}
61 | - name: Run tests
62 | uses: Particular/run-tests-action@v1.7.0
63 | with:
64 | reset-script: dotnet run --project src/targets
65 |
--------------------------------------------------------------------------------
/src/msbuild/ConvertToVersionRange.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.RegularExpressions;
3 | using Microsoft.Build.Framework;
4 | using Microsoft.Build.Utilities;
5 |
6 | public class ConvertToVersionRange : Task
7 | {
8 | [Required]
9 | public ITaskItem[] References { get; set; } = [];
10 |
11 | [Required]
12 | public string VersionProperty { get; set; } = string.Empty;
13 |
14 | [Output]
15 | public ITaskItem[] ReferencesWithVersionRanges { get; private set; } = [];
16 |
17 | public override bool Execute()
18 | {
19 | var success = true;
20 |
21 | foreach (var reference in References)
22 | {
23 | var automaticVersionRange = reference.GetMetadata("AutomaticVersionRange");
24 |
25 | if (automaticVersionRange.Equals("false", StringComparison.OrdinalIgnoreCase))
26 | {
27 | continue;
28 | }
29 |
30 | var privateAssets = reference.GetMetadata("PrivateAssets");
31 |
32 | if (privateAssets.Equals("All", StringComparison.OrdinalIgnoreCase))
33 | {
34 | continue;
35 | }
36 |
37 | var version = reference.GetMetadata(VersionProperty);
38 | var match = Regex.Match(version, @"^\d+");
39 |
40 | if (match.Value.Equals(string.Empty, StringComparison.Ordinal))
41 | {
42 | Log.LogError("Reference '{0}' with version '{1}' is not valid for automatic version range conversion. Fix the version or exclude the reference from conversion by setting 'AutomaticVersionRange=\"false\"' on the reference.", reference.ItemSpec, version);
43 | success = false;
44 | continue;
45 | }
46 |
47 | var nextMajor = Convert.ToInt32(match.Value) + 1;
48 |
49 | var versionRange = $"[{version}, {nextMajor}.0.0)";
50 | reference.SetMetadata(VersionProperty, versionRange);
51 | }
52 |
53 | ReferencesWithVersionRanges = References;
54 |
55 | return success;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/When_using_direct_routing.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests
2 | {
3 | using System.Threading.Tasks;
4 | using AcceptanceTesting;
5 | using NServiceBus.AcceptanceTests;
6 | using NServiceBus.AcceptanceTests.EndpointTemplates;
7 | using NUnit.Framework;
8 |
9 | public class When_using_direct_routing : NServiceBusAcceptanceTest
10 | {
11 | [Test]
12 | public async Task Should_receive_the_message()
13 | {
14 | var context = await Scenario.Define()
15 | .WithEndpoint(b => b.When((bus, c) => bus.SendLocal(new MyRequest())))
16 | .Done(c => c.GotTheMessage)
17 | .Run();
18 |
19 | Assert.That(context.GotTheMessage, Is.True, "Should receive the message");
20 | }
21 |
22 | public class Receiver : EndpointConfigurationBuilder
23 | {
24 | public Receiver()
25 | {
26 | EndpointSetup(c =>
27 | {
28 | c.ConfigureRabbitMQTransport().RoutingTopology = new DirectRoutingTopology(true, QueueType.Classic);
29 | });
30 | }
31 |
32 | class MyEventHandler : IHandleMessages
33 | {
34 | readonly MyContext myContext;
35 |
36 | public MyEventHandler(MyContext myContext)
37 | {
38 | this.myContext = myContext;
39 | }
40 |
41 | public Task Handle(MyRequest message, IMessageHandlerContext context)
42 | {
43 | myContext.GotTheMessage = true;
44 |
45 | return Task.CompletedTask;
46 | }
47 | }
48 | }
49 |
50 | public class MyRequest : IMessage
51 | {
52 | }
53 |
54 | class MyContext : ScenarioContext
55 | {
56 | public bool GotTheMessage { get; set; }
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/When_using_a_custom_message_id_strategy_that_returns_an_invalid_message_id.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests
2 | {
3 | using System.Threading.Tasks;
4 | using AcceptanceTesting;
5 | using NServiceBus.AcceptanceTests;
6 | using NServiceBus.AcceptanceTests.EndpointTemplates;
7 | using NUnit.Framework;
8 |
9 | public class When_using_a_custom_message_id_strategy_that_returns_an_invalid_message_id : NServiceBusAcceptanceTest
10 | {
11 | [Test]
12 | public async Task Should_still_receive_message_if_sent_from_NServiceBus()
13 | {
14 | var context = await Scenario.Define()
15 | .WithEndpoint(c => c.When(session => session.SendLocal(new MyRequest())))
16 | .Done(c => c.GotTheMessage)
17 | .Run();
18 |
19 | Assert.That(context.GotTheMessage, Is.True, "Should receive the message");
20 | }
21 |
22 | public class Receiver : EndpointConfigurationBuilder
23 | {
24 | public Receiver()
25 | {
26 | EndpointSetup(c =>
27 | {
28 | c.ConfigureRabbitMQTransport().MessageIdStrategy = m => "";
29 | });
30 | }
31 |
32 | class MyEventHandler : IHandleMessages
33 | {
34 | readonly MyContext myContext;
35 |
36 | public MyEventHandler(MyContext myContext)
37 | {
38 | this.myContext = myContext;
39 | }
40 |
41 | public Task Handle(MyRequest message, IMessageHandlerContext context)
42 | {
43 | myContext.GotTheMessage = true;
44 |
45 | return Task.CompletedTask;
46 | }
47 | }
48 | }
49 |
50 | public class MyRequest : IMessage
51 | {
52 | }
53 |
54 | class MyContext : ScenarioContext
55 | {
56 | public bool GotTheMessage { get; set; }
57 | }
58 | }
59 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/When_receiving_a_message.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests
2 | {
3 | using System.Threading.Tasks;
4 | using AcceptanceTesting;
5 | using global::RabbitMQ.Client.Events;
6 | using NServiceBus.AcceptanceTests;
7 | using NServiceBus.AcceptanceTests.EndpointTemplates;
8 | using NUnit.Framework;
9 |
10 | class When_receiving_a_message : NServiceBusAcceptanceTest
11 | {
12 | [Test]
13 | public async Task Should_have_access_to_BasicDeliverEventArgs()
14 | {
15 | var scenario = await Scenario.Define()
16 | .WithEndpoint(b => b.When((bus, c) => bus.SendLocal(new Message())))
17 | .Done(c => c.MessageReceived)
18 | .Run();
19 |
20 | Assert.That(scenario.HandlerHasAccessToBasicDeliverEventArgs, Is.True, "The handler should have access to BasicDeliverEventArgs");
21 | }
22 |
23 | public class Receiver : EndpointConfigurationBuilder
24 | {
25 | public Receiver()
26 | {
27 | EndpointSetup();
28 | }
29 |
30 | class MyEventHandler : IHandleMessages
31 | {
32 | Context testContext;
33 |
34 | public MyEventHandler(Context testContext)
35 | {
36 | this.testContext = testContext;
37 | }
38 |
39 | public Task Handle(Message message, IMessageHandlerContext context)
40 | {
41 | testContext.HandlerHasAccessToBasicDeliverEventArgs = context.Extensions.TryGet(out _);
42 | testContext.MessageReceived = true;
43 |
44 | return Task.CompletedTask;
45 | }
46 | }
47 | }
48 |
49 | public class Message : IMessage
50 | {
51 | }
52 |
53 | class Context : ScenarioContext
54 | {
55 | public bool MessageReceived { get; set; }
56 |
57 | public bool HandlerHasAccessToBasicDeliverEventArgs { get; set; }
58 | }
59 | }
60 | }
--------------------------------------------------------------------------------
/src/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | true
7 | true
8 | 5.0
9 | true
10 | low
11 | all
12 |
13 | 2.1.3
14 | 0024000004800000940000000602000000240000525341310004000001000100dde965e6172e019ac82c2639ffe494dd2e7dd16347c34762a05732b492e110f2e4e2e1b5ef2d85c848ccfb671ee20a47c8d1376276708dc30a90ff1121b647ba3b7259a6bc383b2034938ef0e275b58b920375ac605076178123693c6c4f1331661a62eba28c249386855637780e3ff5f23a6d854700eaa6803ef48907513b92
15 | 00240000048000009400000006020000002400005253413100040000010001007f16e21368ff041183fab592d9e8ed37e7be355e93323147a1d29983d6e591b04282e4da0c9e18bd901e112c0033925eb7d7872c2f1706655891c5c9d57297994f707d16ee9a8f40d978f064ee1ffc73c0db3f4712691b23bf596f75130f4ec978cf78757ec034625a5f27e6bb50c618931ea49f6f628fd74271c32959efb1c5
16 |
17 |
18 |
19 | true
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/When_requesting_non_persistent_delivery.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests
2 | {
3 | using System.Threading.Tasks;
4 | using AcceptanceTesting;
5 | using global::RabbitMQ.Client.Events;
6 | using NServiceBus.AcceptanceTests;
7 | using NServiceBus.AcceptanceTests.EndpointTemplates;
8 | using NUnit.Framework;
9 |
10 | class When_requesting_non_persistent_delivery : NServiceBusAcceptanceTest
11 | {
12 | [Test]
13 | public async Task Should_set_delivery_mode_accordingly()
14 | {
15 | var scenario = await Scenario.Define()
16 | .WithEndpoint(b => b.When((bus, c) =>
17 | {
18 | var options = new SendOptions();
19 |
20 | options.RouteToThisEndpoint();
21 | options.UseNonPersistentDeliveryMode();
22 |
23 | return bus.Send(new Message(), options);
24 | }))
25 | .Done(c => c.MessageReceived)
26 | .Run();
27 |
28 | Assert.That(scenario.DeliveryModeWasPersistent, Is.False);
29 | }
30 |
31 | public class Receiver : EndpointConfigurationBuilder
32 | {
33 | public Receiver()
34 | {
35 | EndpointSetup();
36 | }
37 |
38 | class MyEventHandler : IHandleMessages
39 | {
40 | Context testContext;
41 |
42 | public MyEventHandler(Context testContext)
43 | {
44 | this.testContext = testContext;
45 | }
46 |
47 | public Task Handle(Message message, IMessageHandlerContext context)
48 | {
49 | testContext.DeliveryModeWasPersistent = context.Extensions.Get().BasicProperties.Persistent;
50 | testContext.MessageReceived = true;
51 |
52 | return Task.CompletedTask;
53 | }
54 | }
55 | }
56 |
57 | public class Message : IMessage
58 | {
59 | }
60 |
61 | class Context : ScenarioContext
62 | {
63 | public bool MessageReceived { get; set; }
64 |
65 | public bool DeliveryModeWasPersistent { get; set; }
66 | }
67 | }
68 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.CommandLine/BrokerVerifierBinder.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.CommandLine
2 | {
3 | using System.CommandLine;
4 | using NServiceBus.Transport.RabbitMQ.ManagementApi;
5 |
6 | class BrokerVerifierBinder(Option connectionStringOption, Option connectionStringEnvOption, Option managementApiUrlOption, Option managementApiUserNameOption, Option managementApiPasswordOption, Option disableCertificateValidationOption)
7 | {
8 | public BrokerVerifier CreateBrokerVerifier(ParseResult parseResult)
9 | {
10 | var connectionStringOptionValue = parseResult.GetValue(connectionStringOption);
11 | var connectionStringEnvOptionValue = parseResult.GetValue(connectionStringEnvOption);
12 | var managementApiUrl = parseResult.GetValue(managementApiUrlOption);
13 | var managementApiUserName = parseResult.GetValue(managementApiUserNameOption);
14 | var managementApiPassword = parseResult.GetValue(managementApiPasswordOption);
15 | var disableCertificateValidation = parseResult.GetValue(disableCertificateValidationOption);
16 |
17 | var connectionString = GetConnectionString(connectionStringOptionValue, connectionStringEnvOptionValue);
18 |
19 | var connectionConfiguration = ConnectionConfiguration.Create(connectionString);
20 | var managementApiConfiguration = ManagementApiConfiguration.Create(managementApiUrl, managementApiUserName, managementApiPassword);
21 |
22 | var managementClient = new ManagementClient(connectionConfiguration, managementApiConfiguration, disableCertificateValidation);
23 | var brokerVerifier = new BrokerVerifier(managementClient, BrokerRequirementChecks.None, true);
24 |
25 | return brokerVerifier;
26 | }
27 |
28 | static string GetConnectionString(string? connectionString, string? connectionStringEnv)
29 | {
30 | if (string.IsNullOrWhiteSpace(connectionString))
31 | {
32 | var environment = Environment.GetEnvironmentVariable(connectionStringEnv ?? string.Empty);
33 |
34 | if (environment != null)
35 | {
36 | return environment;
37 | }
38 | }
39 |
40 | return connectionString ?? string.Empty;
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/Package-README.md:
--------------------------------------------------------------------------------
1 | ## About this package
2 |
3 | This NuGet package is part of the [Particular Service Platform](https://particular.net/service-platform), which includes [NServiceBus](https://particular.net/nservicebus) and tools to build, monitor, and debug distributed systems.
4 |
5 | Click the **Project website** link in the NuGet sidebar to access specific documentation for this package.
6 |
7 | ## About NServiceBus
8 |
9 | With NServiceBus, you can:
10 |
11 | - Focus on business logic, not on plumbing or infrastructure code
12 | - Orchestrate long-running business processes with sagas
13 | - Run on-premises, in the cloud, in containers, or serverless
14 | - Monitor and respond to failures using included platform tooling
15 | - Observe system performance using Open Telemetry integration
16 |
17 | NServiceBus includes:
18 |
19 | - Support for messages queues using Azure Service Bus, Azure Storage Queues, Amazon SQS/SNS, RabbitMQ, and Microsoft SQL Server
20 | - Support for storing data in Microsoft SQL Server, MySQL, PostgreSQL, Oracle, Azure Cosmos DB, Azure Table Storage, Amazon DynamoDB, MongoDB, and RavenDB
21 | - 24x7 professional support from a team of dedicated engineers located around the world
22 |
23 | ## Getting started
24 |
25 | - Visit the [NServiceBus Quick Start](https://docs.particular.net/tutorials/quickstart/) to learn how NServiceBus helps you build better software systems.
26 | - Visit the [NServiceBus step-by-step tutorial](https://docs.particular.net/tutorials/nservicebus-step-by-step/) to learn how to build NServiceBus systems, including how to send commands, publish events, manage multiple message endpoints, and retry failed messages.
27 | - Install the [ParticularTemplates NuGet package](https://www.nuget.org/packages/ParticularTemplates) to get NServiceBus templates to bootstrap projects using either `dotnet new` or in Visual Studio.
28 | - Check out our other [tutorials](https://docs.particular.net/tutorials/) and [samples](https://docs.particular.net/samples/).
29 | - Get [help with a proof-of-concept](https://particular.net/proof-of-concept).
30 |
31 | ## Packages
32 |
33 | Find links to [all our NuGet packages](https://docs.particular.net/nservicebus/platform-nuget-packages) in our documentation.
34 |
35 | ## Support
36 |
37 | - Browse our [documentation](https://docs.particular.net).
38 | - Reach out to the [ParticularDiscussion](https://discuss.particular.net/) community.
39 | - [Contact us](https://particular.net/support) to discuss your support requirements.
40 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/When_customizing_outgoing_messages.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests
2 | {
3 | using System.Threading.Tasks;
4 | using AcceptanceTesting;
5 | using global::RabbitMQ.Client.Events;
6 | using NServiceBus.AcceptanceTests;
7 | using NServiceBus.AcceptanceTests.EndpointTemplates;
8 | using NUnit.Framework;
9 |
10 | class When_customizing_outgoing_messages : NServiceBusAcceptanceTest
11 | {
12 | [Test]
13 | public async Task Should_set_value()
14 | {
15 | var scenario = await Scenario.Define()
16 | .WithEndpoint(b => b.When((bus, c) => bus.SendLocal(new Message())))
17 | .Done(c => c.MessageReceived)
18 | .Run();
19 |
20 | Assert.That(scenario.BasicDeliverEventArgs.BasicProperties.AppId, Is.EqualTo("MyValue"));
21 | }
22 |
23 | public class Receiver : EndpointConfigurationBuilder
24 | {
25 | public Receiver()
26 | {
27 | EndpointSetup(endpointConfiguration =>
28 | {
29 | endpointConfiguration.ConfigureRabbitMQTransport().OutgoingNativeMessageCustomization =
30 | (operation, properties) =>
31 | {
32 | properties.AppId = "MyValue";
33 | };
34 | });
35 | }
36 |
37 | class MyEventHandler : IHandleMessages
38 | {
39 | Context testContext;
40 |
41 | public MyEventHandler(Context testContext)
42 | {
43 | this.testContext = testContext;
44 | }
45 |
46 | public Task Handle(Message message, IMessageHandlerContext context)
47 | {
48 | testContext.BasicDeliverEventArgs = context.Extensions.Get();
49 | testContext.MessageReceived = true;
50 |
51 | return Task.CompletedTask;
52 | }
53 | }
54 | }
55 |
56 | public class Message : IMessage
57 | {
58 | }
59 |
60 | class Context : ScenarioContext
61 | {
62 | public bool MessageReceived { get; set; }
63 |
64 | public BasicDeliverEventArgs BasicDeliverEventArgs { get; set; }
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Administration/ManagementApi/Models/Queue.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | namespace NServiceBus.Transport.RabbitMQ.ManagementApi;
4 |
5 | using System.Text.Json.Serialization;
6 |
7 | class Queue
8 | {
9 | [JsonPropertyName("name")]
10 | public required string Name { get; set; }
11 |
12 | [JsonPropertyName("type")]
13 | [JsonConverter(typeof(QueueTypeConverter))]
14 | public QueueType? QueueType { get; set; }
15 |
16 | [JsonPropertyName("arguments")]
17 | public required QueueArguments Arguments { get; set; }
18 |
19 | [JsonPropertyName("delivery_limit")]
20 | [JsonConverter(typeof(DeliveryLimitConverter))]
21 | public int? DeliveryLimit { get; set; }
22 |
23 | [JsonPropertyName("effective_policy_definition")]
24 | public PolicyDefinition? EffectivePolicyDefinition { get; set; }
25 |
26 | [JsonPropertyName("policy")]
27 | public string? AppliedPolicyName { get; set; }
28 |
29 | // For ServiceControl queue length provider
30 | [JsonPropertyName("messages")]
31 | public long MessageCount { get; set; }
32 |
33 | // For ServiceControl licensing component
34 | [JsonPropertyName("message_stats")]
35 | public QueueMessageStats? MessageStats { get; set; }
36 |
37 | public int GetDeliveryLimit()
38 | {
39 | // RabbitMQ 4.x
40 | if (DeliveryLimit is not null)
41 | {
42 | return DeliveryLimit.Value;
43 | }
44 |
45 | // RabbitMQ 3.x
46 | // The broker doesn't tell us what the actual delivery limit is, so we have to figure it out.
47 | // We have to find the lowest value from the possible places it can be configured.
48 |
49 | int? limit = null;
50 |
51 | if (EffectivePolicyDefinition?.DeliveryLimit is not null)
52 | {
53 | limit = EffectivePolicyDefinition.DeliveryLimit;
54 | }
55 |
56 | if (Arguments.DeliveryLimit is not null)
57 | {
58 | if (limit is null || (limit is not null && Arguments.DeliveryLimit < limit))
59 | {
60 | limit = Arguments.DeliveryLimit;
61 | }
62 | }
63 |
64 | // queue argument can be negative but is still treated as 0 on 3.x
65 | if (limit is not null and < 0)
66 | {
67 | limit = 0;
68 | }
69 |
70 | return limit ?? BigValueInsteadOfActuallyUnlimited;
71 | }
72 |
73 | public const int BigValueInsteadOfActuallyUnlimited = 100_000;
74 | }
75 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Receiving/MessagePumpConnectionFailedCircuitBreaker.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ
2 | {
3 | using System;
4 | using System.Threading;
5 | using Logging;
6 |
7 | sealed class MessagePumpConnectionFailedCircuitBreaker : IDisposable
8 | {
9 | public MessagePumpConnectionFailedCircuitBreaker(string name, TimeSpan timeToWaitBeforeTriggering, Action criticalErrorAction)
10 | {
11 | this.name = name;
12 | this.criticalErrorAction = criticalErrorAction;
13 | this.timeToWaitBeforeTriggering = timeToWaitBeforeTriggering;
14 |
15 | timer = new Timer(CircuitBreakerTriggered);
16 | }
17 |
18 | public bool Disarmed => Interlocked.Read(ref failureCount) == 0;
19 |
20 | public void Success()
21 | {
22 | var oldValue = Interlocked.Exchange(ref failureCount, 0);
23 |
24 | if (oldValue == 0)
25 | {
26 | return;
27 | }
28 |
29 | timer.Change(Timeout.Infinite, Timeout.Infinite);
30 | Logger.InfoFormat("The circuit breaker for '{0}' is now disarmed", name);
31 | }
32 |
33 | public void Failure(Exception exception)
34 | {
35 | lastException = exception;
36 | var newValue = Interlocked.Increment(ref failureCount);
37 |
38 | if (newValue == 1)
39 | {
40 | timer.Change(timeToWaitBeforeTriggering, NoPeriodicTriggering);
41 | Logger.WarnFormat("The circuit breaker for '{0}' is now in the armed state", name);
42 | }
43 | }
44 |
45 | public void Dispose()
46 | {
47 | timer?.Dispose();
48 | }
49 |
50 | void CircuitBreakerTriggered(object state)
51 | {
52 | if (Interlocked.Read(ref failureCount) > 0)
53 | {
54 | Logger.WarnFormat("The circuit breaker for '{0}' will now be triggered", name);
55 | criticalErrorAction($"'{name}' connection to the broker has failed.", lastException);
56 | }
57 | }
58 |
59 | static readonly TimeSpan NoPeriodicTriggering = TimeSpan.FromMilliseconds(-1);
60 | static readonly ILog Logger = LogManager.GetLogger();
61 | string name;
62 | TimeSpan timeToWaitBeforeTriggering;
63 | Timer timer;
64 | Action criticalErrorAction;
65 | long failureCount;
66 | Exception lastException;
67 | }
68 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.Tests/DelayedDelivery/When_calculating_a_routing_key.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.Tests.DelayedDelivery
2 | {
3 | using NUnit.Framework;
4 |
5 | [TestFixture]
6 | public class When_calculating_a_routing_key
7 | {
8 | [TestCase(0, "some-address", "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.some-address", 0)]
9 | [TestCase(0, "", "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.", 0)]
10 | [TestCase(0, "exotic-😊-address", "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.exotic-😊-address", 0)]
11 | [TestCase(1, "some-address", "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.some-address", 0)]
12 | [TestCase(2, "some-address", "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.some-address", 1)]
13 | [TestCase(3, "🔥-unicode", "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.1.🔥-unicode", 1)]
14 | [TestCase(10, "some-address", "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.1.0.some-address", 3)]
15 | [TestCase(DelayInfrastructure.MaxDelayInSeconds - 1, "almost-max", "1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.0.almost-max", 27)]
16 | [TestCase(DelayInfrastructure.MaxDelayInSeconds, "some-address", "1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1.some-address", 27)]
17 | [TestCase(28, "short-delay", "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.1.1.0.0.short-delay", 4)]
18 | [TestCase(15, "small-delay", "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.1.1.1.small-delay", 3)]
19 | public void Should_return_correct_routing_key_based_on_delay(int delayInSeconds, string address, string expectedRoutingKey, int expectedStartingDelayLevel)
20 | {
21 | var result = DelayInfrastructure.CalculateRoutingKey(delayInSeconds, address, out var startingDelayLevel);
22 |
23 | Assert.Multiple(() =>
24 | {
25 | Assert.That(result, Is.EqualTo(expectedRoutingKey));
26 | Assert.That(startingDelayLevel, Is.EqualTo(expectedStartingDelayLevel));
27 | });
28 | }
29 |
30 | [Test]
31 | public void Should_return_routing_key_with_delay_zero_seconds_for_negative_delay()
32 | {
33 | var result = DelayInfrastructure.CalculateRoutingKey(-123, "some-address", out var startingDelayLevel);
34 |
35 | Assert.Multiple(() =>
36 | {
37 | Assert.That(result, Is.EqualTo("0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.some-address"));
38 | Assert.That(startingDelayLevel, Is.EqualTo(0));
39 | });
40 | }
41 | }
42 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.TransportTests/ConfigureRabbitMQTransportInfrastructure.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading;
3 | using System.Threading.Tasks;
4 | using NServiceBus;
5 | using NServiceBus.Transport;
6 | using NServiceBus.TransportTests;
7 |
8 | // Workaround to prevent errors because scanning expects this type to exist
9 | class ConfigureRabbitMQClusterTransportInfrastructure : ConfigureRabbitMQTransportInfrastructure
10 | {
11 | }
12 |
13 | class ConfigureRabbitMQTransportInfrastructure : IConfigureTransportInfrastructure
14 | {
15 | public TransportDefinition CreateTransportDefinition()
16 | {
17 | var connectionString = Environment.GetEnvironmentVariable("RabbitMQTransport_ConnectionString") ?? "host=localhost";
18 |
19 | var transport = new RabbitMQTransport(RoutingTopology.Conventional(QueueType.Classic), connectionString, false)
20 | {
21 | // The startup costs for creating a policy for every test queue add up, and the tests shouldn't be impacted by the default delivery limit.
22 | ValidateDeliveryLimits = false
23 | };
24 |
25 | return transport;
26 | }
27 |
28 | public async Task Configure(TransportDefinition transportDefinition, HostSettings hostSettings, QueueAddress inputQueue, string errorQueueName, CancellationToken cancellationToken = default)
29 | {
30 | var mainReceiverSettings = new ReceiveSettings(
31 | "mainReceiver",
32 | inputQueue,
33 | true,
34 | false,
35 | errorQueueName);
36 |
37 | var transport = await transportDefinition.Initialize(hostSettings, [mainReceiverSettings], [errorQueueName], cancellationToken);
38 |
39 | queuesToCleanUp = [transport.ToTransportAddress(inputQueue), errorQueueName];
40 | return transport;
41 | }
42 |
43 | public async Task Cleanup(CancellationToken cancellationToken = default)
44 | {
45 | if (queuesToCleanUp == null)
46 | {
47 | return;
48 | }
49 |
50 | using var connection = await ConnectionHelper.ConnectionFactory.CreateConnectionAsync("Test Queue Purger", cancellationToken);
51 | using var channel = await connection.CreateChannelAsync(cancellationToken: cancellationToken);
52 | foreach (var queue in queuesToCleanUp)
53 | {
54 | try
55 | {
56 | await channel.QueuePurgeAsync(queue, cancellationToken);
57 | }
58 | catch (Exception ex) when (!ex.IsCausedBy(cancellationToken))
59 | {
60 | Console.WriteLine("Unable to clear queue {0}: {1}", queue, ex);
61 | }
62 | }
63 | }
64 |
65 | string[] queuesToCleanUp;
66 | }
--------------------------------------------------------------------------------
/src/msbuild/AutomaticVersionRanges.targets:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | false
5 | false
6 | false
7 | true
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | @(_ProjectReferencesWithVersions->Count())
19 |
20 |
21 |
22 |
23 |
24 | <_ProjectReferencesWithVersions Remove="@(_ProjectReferencesWithVersions)" />
25 | <_ProjectReferencesWithVersions Include="@(_ProjectReferencesWithVersionRanges)" />
26 |
27 |
28 |
29 |
30 |
31 | @(PackageReference->Count())
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Configuration/RoutingTopology.cs:
--------------------------------------------------------------------------------
1 | #nullable disable
2 | namespace NServiceBus
3 | {
4 | using System;
5 | using NServiceBus.Transport.RabbitMQ;
6 |
7 | ///
8 | /// Configures the transport to use the specified routing topology.
9 | ///
10 | public class RoutingTopology
11 | {
12 | readonly Func routingTopology;
13 |
14 | RoutingTopology(Func routingTopology)
15 | {
16 | this.routingTopology = routingTopology;
17 | }
18 |
19 | ///
20 | /// Configures the transport to use the conventional routing topology.
21 | ///
22 | /// The type of queue that the endpoint should use.
23 | /// Specifies whether exchanges and queues should be declared as durable or not.
24 | public static RoutingTopology Conventional(QueueType queueType, bool useDurableEntities = true)
25 | {
26 | return new RoutingTopology(() => new ConventionalRoutingTopology(useDurableEntities, queueType));
27 | }
28 |
29 | internal static RoutingTopology Conventional(QueueType queueType, Func exchangeNameConvention, bool useDurable = true)
30 | {
31 | return new RoutingTopology(() => new ConventionalRoutingTopology(useDurable, queueType, exchangeNameConvention));
32 | }
33 |
34 | ///
35 | /// Configures the transport to use the direct routing topology.
36 | ///
37 | /// The type of queue to use.
38 | /// Specifies whether exchanges and queues should be declared as durable or not.
39 | /// The routing key convention.
40 | /// The exchange name convention.
41 | public static RoutingTopology Direct(QueueType queueType, bool useDurableEntities = true, Func routingKeyConvention = null, Func exchangeNameConvention = null)
42 | {
43 | return new RoutingTopology(() => new DirectRoutingTopology(useDurableEntities, queueType, routingKeyConvention, exchangeNameConvention));
44 | }
45 |
46 | ///
47 | /// Configures the transport to use a custom routing topology.
48 | ///
49 | /// The custom routing topology instance to use.
50 | public static RoutingTopology Custom(IRoutingTopology routingTopology)
51 | {
52 | return new RoutingTopology(() => routingTopology);
53 | }
54 |
55 | internal IRoutingTopology Create()
56 | {
57 | return routingTopology();
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.Tests/When_consuming_from_classic_queues.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.Tests
2 | {
3 | using System;
4 | using System.Threading.Tasks;
5 | using global::RabbitMQ.Client;
6 | using global::RabbitMQ.Client.Events;
7 | using NUnit.Framework;
8 |
9 | [TestFixture]
10 | class When_consuming_from_classic_queues : RabbitMqContext
11 | {
12 | [SetUp]
13 | public override Task SetUp()
14 | {
15 | queueType = QueueType.Classic;
16 | return base.SetUp();
17 | }
18 |
19 | [Test]
20 | public async Task Header_collection_is_null_after_redelivery_for_headerless_messages()
21 | {
22 | var message = new OutgoingMessage(Guid.NewGuid().ToString(), [], Array.Empty());
23 | var headerCollectionWasNullOnFirstDelivery = false;
24 | var headerCollectionWasNullOnRedelivery = new TaskCompletionSource();
25 |
26 | OnMessage = (mc, __) =>
27 | {
28 | var basicDeliverEventArgs = mc.Extensions.Get();
29 |
30 | if (!basicDeliverEventArgs.Redelivered)
31 | {
32 | headerCollectionWasNullOnFirstDelivery = basicDeliverEventArgs.BasicProperties.Headers == null;
33 | throw new Exception("Some failure");
34 | }
35 |
36 | headerCollectionWasNullOnRedelivery.SetResult(basicDeliverEventArgs.BasicProperties.Headers == null);
37 |
38 | return Task.CompletedTask;
39 | };
40 |
41 | OnError = (ec, __) => Task.FromResult(ErrorHandleResult.RetryRequired);
42 |
43 | using (var connection = await connectionFactory.CreatePublishConnection())
44 | using (var channel = await connection.CreateChannelAsync())
45 | {
46 | var properties = new BasicProperties
47 | {
48 | MessageId = message.MessageId
49 | };
50 |
51 | await channel.BasicPublishAsync(string.Empty, ReceiverQueue, false, properties, message.Body);
52 |
53 | if (await Task.WhenAny(headerCollectionWasNullOnRedelivery.Task, Task.Delay(IncomingMessageTimeout)) != headerCollectionWasNullOnRedelivery.Task)
54 | {
55 | Assert.Fail("Message receive timed out");
56 | }
57 |
58 | var headersWasNullOnRedelivery = await headerCollectionWasNullOnRedelivery.Task;
59 |
60 | Assert.Multiple(() =>
61 | {
62 | Assert.That(headerCollectionWasNullOnFirstDelivery, Is.True, "Header collection should be null on the first delivery");
63 | Assert.That(headersWasNullOnRedelivery, Is.True, "Header collection should be null even after a redelivery");
64 | });
65 | }
66 | }
67 |
68 | class MyMessage
69 | {
70 | }
71 | }
72 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.Tests/OutgoingMessageBuilder.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.Tests
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using Performance.TimeToBeReceived;
6 | using Routing;
7 |
8 | public class OutgoingMessageBuilder
9 | {
10 | public OutgoingMessageBuilder WithBody(byte[] body)
11 | {
12 | this.body = body;
13 | return this;
14 | }
15 |
16 | public TransportOperations Build(int copies = 1)
17 | {
18 | var message = new OutgoingMessage(messageId, headers, body);
19 |
20 | var transportOperations = new List();
21 |
22 | for (var i = 0; i < copies; i++)
23 | {
24 | if (eventType != null)
25 | {
26 | transportOperations.Add(new TransportOperation(message, new MulticastAddressTag(eventType), constraints, dispatchConsistency));
27 | }
28 |
29 | if (!string.IsNullOrEmpty(destination))
30 | {
31 | transportOperations.Add(new TransportOperation(message, new UnicastAddressTag(destination), constraints, dispatchConsistency));
32 | }
33 | }
34 |
35 | return new TransportOperations(transportOperations.ToArray());
36 | }
37 |
38 | public OutgoingMessageBuilder SendTo(string unicastAddress)
39 | {
40 | destination = unicastAddress;
41 | return this;
42 | }
43 |
44 | public OutgoingMessageBuilder PublishType(Type messageType)
45 | {
46 | eventType = messageType;
47 | return this;
48 | }
49 |
50 | public OutgoingMessageBuilder WithHeader(string key, string value)
51 | {
52 | headers[key] = value;
53 | return this;
54 | }
55 |
56 | public OutgoingMessageBuilder TimeToBeReceived(TimeSpan timeToBeReceived)
57 | {
58 | constraints.DiscardIfNotReceivedBefore = new DiscardIfNotReceivedBefore(timeToBeReceived);
59 | return this;
60 | }
61 |
62 | public OutgoingMessageBuilder ReplyToAddress(string address)
63 | {
64 | return WithHeader(Headers.ReplyToAddress, address);
65 | }
66 |
67 | public OutgoingMessageBuilder CorrelationId(string correlationId)
68 | {
69 | return WithHeader(Headers.CorrelationId, correlationId);
70 | }
71 |
72 | public OutgoingMessageBuilder WithIntent(MessageIntent intent)
73 | {
74 | return WithHeader(Headers.MessageIntent, intent.ToString());
75 | }
76 |
77 | string destination;
78 | Type eventType;
79 | string messageId = Guid.NewGuid().ToString();
80 | byte[] body;
81 | Dictionary headers = [];
82 | DispatchProperties constraints = [];
83 | DispatchConsistency dispatchConsistency = DispatchConsistency.Default;
84 | }
85 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Routing/DefaultRoutingKeyConvention.cs:
--------------------------------------------------------------------------------
1 | #nullable disable
2 | namespace NServiceBus.Transport.RabbitMQ
3 | {
4 | using System;
5 | using System.Collections;
6 | using System.Collections.Concurrent;
7 | using System.Linq;
8 | using Logging;
9 |
10 | static class DefaultRoutingKeyConvention
11 | {
12 | public static string GenerateRoutingKey(Type eventType) =>
13 | EventTypeToRoutingKeyCache.GetOrAdd(eventType, static eventType => GetRoutingKey(eventType));
14 |
15 | static string GetRoutingKey(Type type, string key = "")
16 | {
17 | var baseType = type.BaseType;
18 |
19 | if (baseType != null && !IsSystemType(baseType))
20 | {
21 | key = GetRoutingKey(baseType, key);
22 | }
23 |
24 | var interfaces = type.GetInterfaces()
25 | .Where(i => !IsSystemType(i) && !IsNServiceBusMarkerInterface(i)).ToList();
26 |
27 | var implementedInterface = interfaces.FirstOrDefault();
28 |
29 | if (interfaces.Count > 1)
30 | {
31 | Logger.WarnFormat("The direct routing topology cannot properly publish a message type that implements more than one relevant interface. The type '{0}' implements the following interfaces: {1}. The interface that will be used is '{2}'. The others will be ignored, and any endpoints that subscribe to those interfaces will not receive a copy of the message.", type, string.Join(", ", interfaces), implementedInterface);
32 | }
33 |
34 | if (implementedInterface != null)
35 | {
36 | key = GetRoutingKey(implementedInterface, key);
37 | }
38 |
39 | if (!string.IsNullOrEmpty(key))
40 | {
41 | key += ".";
42 | }
43 |
44 | return key + type.FullName.Replace(".", "-");
45 | }
46 |
47 | static bool IsSystemType(Type type) =>
48 | IsSystemTypeCache.GetOrAdd(type, static type =>
49 | {
50 | var nameOfContainingAssembly = type.Assembly.GetName().GetPublicKeyToken();
51 | return IsClrType(nameOfContainingAssembly);
52 | });
53 |
54 | static bool IsClrType(byte[] a1)
55 | {
56 | IStructuralEquatable structuralEquatable = a1;
57 | return structuralEquatable.Equals(MsPublicKeyToken, StructuralComparisons.StructuralEqualityComparer);
58 | }
59 |
60 | static bool IsNServiceBusMarkerInterface(Type type) => type == typeof(IMessage) || type == typeof(ICommand) || type == typeof(IEvent);
61 |
62 | static readonly byte[] MsPublicKeyToken = typeof(string).Assembly.GetName().GetPublicKeyToken();
63 | static readonly ConcurrentDictionary IsSystemTypeCache = new();
64 | static readonly ConcurrentDictionary EventTypeToRoutingKeyCache = new();
65 | static readonly ILog Logger = LogManager.GetLogger(typeof(DefaultRoutingKeyConvention));
66 | }
67 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/When_scaling_out_subscribers.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests
2 | {
3 | using System;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using AcceptanceTesting;
7 | using NServiceBus.AcceptanceTests;
8 | using NServiceBus.AcceptanceTests.EndpointTemplates;
9 | using NUnit.Framework;
10 |
11 | public class When_scaling_out_subscribers : NServiceBusAcceptanceTest
12 | {
13 | [Test]
14 | public async Task Should_only_deliver_event_to_one_of_the_instances()
15 | {
16 | var context = await Scenario.Define()
17 | .WithEndpoint(b => b.When(c => c.ServerASubscribed && c.ServerBSubscribed, bus => bus.Publish()))
18 | .WithEndpoint(b =>
19 | {
20 | b.CustomConfig(c => c.MakeInstanceUniquelyAddressable("A"));
21 | b.When(async (bus, c) =>
22 | {
23 | await bus.Subscribe();
24 | c.ServerASubscribed = true;
25 | });
26 | })
27 | .WithEndpoint(b =>
28 | {
29 | b.CustomConfig(c => c.MakeInstanceUniquelyAddressable("B"));
30 | b.When(async (bus, c) =>
31 | {
32 | await bus.Subscribe();
33 | c.ServerBSubscribed = true;
34 | });
35 | })
36 | .Done(ctx => ctx.Counter > 0)
37 | .Run(TimeSpan.FromSeconds(10));
38 |
39 | Assert.That(context.Counter, Is.EqualTo(1), "One of the scaled out instances should get the event");
40 | }
41 |
42 | public class ScaledOutSubscriber : EndpointConfigurationBuilder
43 | {
44 | public ScaledOutSubscriber() => EndpointSetup();
45 |
46 | class MyEventHandler(MyContext myContext) : IHandleMessages
47 | {
48 | public Task Handle(MyEvent message, IMessageHandlerContext context)
49 | {
50 | myContext.IncrementCounter();
51 | return Task.CompletedTask;
52 | }
53 | }
54 | }
55 |
56 | public class Publisher : EndpointConfigurationBuilder
57 | {
58 | public Publisher() => EndpointSetup();
59 | }
60 |
61 | public class MyEvent : IEvent;
62 |
63 | class MyContext : ScenarioContext
64 | {
65 | public bool ServerASubscribed { get; set; }
66 | public bool ServerBSubscribed { get; set; }
67 | public int Counter => counter;
68 |
69 | public void IncrementCounter() => Interlocked.Increment(ref counter);
70 |
71 | int counter;
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.CommandLine/NServiceBus.Transport.RabbitMQ.CommandLine.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net10.0
5 | Exe
6 | rabbitmq-transport
7 | True
8 | A .NET global tool to manage the RabbitMQ transport for NServiceBus endpoints
9 | false
10 | enable
11 | enable
12 | true
13 | ..\NServiceBus.snk
14 | true
15 | $(DefineConstants);COMMANDLINE
16 | Major
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/ConfigureEndpointRabbitMQTransport.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using NServiceBus;
7 | using NServiceBus.AcceptanceTesting.Support;
8 | using NServiceBus.Transport;
9 | using NServiceBus.Transport.RabbitMQ.AcceptanceTests;
10 |
11 | class ConfigureEndpointRabbitMQTransport : IConfigureEndpointTestExecution
12 | {
13 | TestRabbitMQTransport transport;
14 | readonly QueueType queueType;
15 |
16 | public ConfigureEndpointRabbitMQTransport(QueueType queueType = QueueType.Classic)
17 | {
18 | this.queueType = queueType;
19 | }
20 |
21 | public Task Configure(string endpointName, EndpointConfiguration configuration, RunSettings settings, PublisherMetadata publisherMetadata)
22 | {
23 | transport = new TestRabbitMQTransport(RoutingTopology.Conventional(queueType, type => type.FullName), ConnectionHelper.ConnectionString)
24 | {
25 | // The startup costs for creating a policy for every test queue add up, and the tests shouldn't be impacted by the default delivery limit.
26 | ValidateDeliveryLimits = false
27 | };
28 |
29 | configuration.UseTransport(transport);
30 |
31 | return Task.CompletedTask;
32 | }
33 |
34 | public async Task Cleanup()
35 | {
36 | await PurgeQueues();
37 | }
38 |
39 | async Task PurgeQueues()
40 | {
41 | if (transport == null)
42 | {
43 | return;
44 | }
45 |
46 | var queues = transport.QueuesToCleanup.ToHashSet();
47 |
48 | using (var connection = await ConnectionHelper.ConnectionFactory.CreateConnectionAsync("Test Queue Purger"))
49 | using (var channel = await connection.CreateChannelAsync())
50 | {
51 | foreach (var queue in queues)
52 | {
53 | try
54 | {
55 | await channel.QueuePurgeAsync(queue);
56 | }
57 | catch (Exception ex)
58 | {
59 | Console.WriteLine("Unable to clear queue {0}: {1}", queue, ex);
60 | }
61 | }
62 | }
63 | }
64 |
65 | class TestRabbitMQTransport : RabbitMQTransport
66 | {
67 | public TestRabbitMQTransport(RoutingTopology routingTopology, string connectionString)
68 | : base(routingTopology, connectionString)
69 | {
70 | }
71 |
72 | public override async Task Initialize(HostSettings hostSettings, ReceiveSettings[] receivers, string[] sendingAddresses, CancellationToken cancellationToken = default)
73 | {
74 | var infrastructure = await base.Initialize(hostSettings, receivers, sendingAddresses, cancellationToken);
75 | QueuesToCleanup.AddRange(infrastructure.Receivers.Select(x => x.Value.ReceiveAddress).Concat(sendingAddresses).Distinct());
76 | return infrastructure;
77 | }
78 |
79 | public List QueuesToCleanup { get; } = [];
80 | }
81 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Connection/ConfirmsAwareChannel.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using global::RabbitMQ.Client;
8 |
9 | sealed class ConfirmsAwareChannel(IConnection connection, IRoutingTopology routingTopology) : IAsyncDisposable
10 | {
11 | public bool IsOpen => channel.IsOpen;
12 |
13 | public bool IsClosed => channel.IsClosed;
14 |
15 | public async Task Initialize(CancellationToken cancellationToken = default)
16 | {
17 | var createChannelOptions = new CreateChannelOptions(publisherConfirmationsEnabled: true, publisherConfirmationTrackingEnabled: true, outstandingPublisherConfirmationsRateLimiter: null);
18 | channel = await connection.CreateChannelAsync(createChannelOptions, cancellationToken: cancellationToken).ConfigureAwait(false);
19 | }
20 |
21 | public async ValueTask SendMessage(string address, OutgoingMessage message, BasicProperties properties, CancellationToken cancellationToken = default)
22 | {
23 | if (properties.Headers != null &&
24 | properties.Headers.TryGetValue(DelayInfrastructure.DelayHeader, out var delayValue))
25 | {
26 | var routingKey =
27 | DelayInfrastructure.CalculateRoutingKey((int)delayValue, address, out var startingDelayLevel);
28 |
29 | await routingTopology.BindToDelayInfrastructure(channel, address,
30 | DelayInfrastructure.DeliveryExchange, DelayInfrastructure.BindingKey(address),
31 | cancellationToken).ConfigureAwait(false);
32 | // The channel is used here directly because it is not the routing topologies concern to know about the sends to the delay infrastructure
33 | await channel.BasicPublishAsync(DelayInfrastructure.LevelName(startingDelayLevel), routingKey, true,
34 | properties, message.Body, cancellationToken).ConfigureAwait(false);
35 | }
36 | else
37 | {
38 | await routingTopology.Send(channel, address, message, properties, cancellationToken)
39 | .ConfigureAwait(false);
40 | }
41 | }
42 |
43 | public async ValueTask PublishMessage(Type type, OutgoingMessage message, BasicProperties properties, CancellationToken cancellationToken = default) =>
44 | await routingTopology.Publish(channel, type, message, properties, cancellationToken)
45 | .ConfigureAwait(false);
46 |
47 | public async ValueTask RawSendInCaseOfFailure(string address, ReadOnlyMemory body, BasicProperties properties, CancellationToken cancellationToken = default)
48 | {
49 | properties.Headers ??= new Dictionary();
50 |
51 | await routingTopology.RawSendInCaseOfFailure(channel, address, body, properties, cancellationToken)
52 | .ConfigureAwait(false);
53 | }
54 |
55 | #pragma warning disable PS0018
56 | public ValueTask DisposeAsync() => channel is not null ? channel.DisposeAsync() : ValueTask.CompletedTask;
57 | #pragma warning restore PS0018
58 |
59 | IChannel channel;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.CommandLine.Tests/MigrateDelayedMessages/When_migrating_a_delayed_message.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.CommandLine.Tests.MigrateDelayedMessages
2 | {
3 | using System;
4 | using NUnit.Framework;
5 |
6 | [TestFixture]
7 | public class When_migrating_a_delayed_message
8 | {
9 | [Test]
10 | [TestCase(10, new int[] { 2022, 5, 6, 12, 0, 0 }, "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.1.0.destination", new int[] { 2022, 5, 6, 12, 0, 10 }, "destination", "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.destination", 0)]
11 | [TestCase(32, new int[] { 2022, 5, 6, 12, 0, 0 }, "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.0.0.destination", new int[] { 2022, 5, 6, 12, 0, 30 }, "destination", "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.destination", 1)]
12 | [TestCase(64, new int[] { 2022, 5, 6, 12, 0, 0 }, "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.0.0.0.destination", new int[] { 2022, 5, 6, 12, 0, 30 }, "destination", "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.1.0.destination", 5)]
13 | [TestCase(128, new int[] { 2022, 5, 6, 12, 0, 0 }, "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.0.0.0.0.0.0.destination", new int[] { 2022, 5, 6, 12, 0, 30 }, "destination", "0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.1.0.0.0.1.0.destination", 6)]
14 | [TestCase(86400, new int[] { 2022, 5, 6, 12, 0, 0 }, "0.0.0.0.0.0.0.0.0.0.0.1.0.1.0.1.0.0.0.1.1.0.0.0.0.0.0.0.destination", new int[] { 2022, 5, 7, 0, 0, 0 }, "destination", "0.0.0.0.0.0.0.0.0.0.0.0.1.0.1.0.1.0.0.0.1.1.0.0.0.0.0.0.destination", 15)]
15 | [TestCase(604800, new int[] { 2022, 5, 6, 12, 0, 0 }, "0.0.0.0.0.0.0.0.1.0.0.1.0.0.1.1.1.0.1.0.1.0.0.0.0.0.0.0.destination", new int[] { 2022, 5, 9, 12, 0, 0 }, "destination", "0.0.0.0.0.0.0.0.0.1.0.1.0.1.0.0.0.1.1.0.0.0.0.0.0.0.0.0.destination", 18)]
16 | [TestCase(604800, new int[] { 2022, 5, 6, 12, 0, 0 }, "0.0.0.0.0.0.0.0.1.0.0.1.0.0.1.1.1.0.1.0.1.0.0.0.0.0.0.0.destination.with.dots.in.name", new int[] { 2022, 5, 9, 12, 0, 0 }, "destination.with.dots.in.name", "0.0.0.0.0.0.0.0.0.1.0.1.0.1.0.0.0.1.1.0.0.0.0.0.0.0.0.0.destination.with.dots.in.name", 18)]
17 | public void Should_generate_the_correct_routing_key(int originalDelayInSeconds, int[] originalTimeSentValues, string originalRoutingKey, int[] utcNowValues, string expectedDestination, string expectedRoutingKey, int expectedDelayLevel)
18 | {
19 | var originalTimeSent = GetDateTimeOffsetFromValues(originalTimeSentValues);
20 | var utcNow = GetDateTimeOffsetFromValues(utcNowValues);
21 |
22 | var (destinationQueue, newRoutingKey, newDelayLevel) = DelaysMigrateCommand.GetNewRoutingKey(originalDelayInSeconds, originalTimeSent, originalRoutingKey, utcNow);
23 |
24 | Assert.Multiple(() =>
25 | {
26 | Assert.That(destinationQueue, Is.EqualTo(expectedDestination));
27 | Assert.That(newRoutingKey, Is.EqualTo(expectedRoutingKey));
28 | Assert.That(newDelayLevel, Is.EqualTo(expectedDelayLevel));
29 | });
30 | }
31 |
32 | DateTimeOffset GetDateTimeOffsetFromValues(int[] values)
33 | {
34 | return new DateTimeOffset(values[0], values[1], values[2], values[3], values[4], values[5], TimeSpan.Zero);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.CommandLine/BrokerConnectionBinder.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.CommandLine
2 | {
3 | using System.CommandLine;
4 | using System.Security.Cryptography.X509Certificates;
5 | using NServiceBus.Transport.RabbitMQ.ManagementApi;
6 |
7 | class BrokerConnectionBinder(Option connectionStringOption, Option connectionStringEnvOption, Option managementApiUrlOption, Option managementApiUserNameOption, Option managementApiPasswordOption, Option certPathOption,
8 | Option certPassphraseOption, Option disableCertificateValidationOption, Option useExternalAuthOption)
9 | {
10 | public BrokerConnection CreateBrokerConnection(ParseResult parseResult)
11 | {
12 | var connectionStringOptionValue = parseResult.GetValue(connectionStringOption);
13 | var connectionStringEnvOptionValue = parseResult.GetValue(connectionStringEnvOption);
14 | var managementApiUrl = parseResult.GetValue(managementApiUrlOption);
15 | var managementApiUserName = parseResult.GetValue(managementApiUserNameOption);
16 | var managementApiPassword = parseResult.GetValue(managementApiPasswordOption);
17 | var certPath = parseResult.GetValue(certPathOption);
18 | var certPassphrase = parseResult.GetValue(certPassphraseOption);
19 | var disableCertificateValidation = parseResult.GetValue(disableCertificateValidationOption);
20 | var useExternalAuth = parseResult.GetValue(useExternalAuthOption);
21 |
22 | var connectionString = GetConnectionString(connectionStringOptionValue, connectionStringEnvOptionValue);
23 |
24 | var connectionConfiguration = ConnectionConfiguration.Create(connectionString);
25 | var managementApiConfiguration = ManagementApiConfiguration.Create(managementApiUrl, managementApiUserName, managementApiPassword);
26 |
27 | var managementClient = new ManagementClient(connectionConfiguration, managementApiConfiguration, disableCertificateValidation);
28 | var brokerVerifier = new BrokerVerifier(managementClient, BrokerRequirementChecks.None, true);
29 |
30 | X509Certificate2Collection? certificateCollection = null;
31 |
32 | if (certPath is not null)
33 | {
34 | certificateCollection = [CertificateLoader.LoadCertificateFromFile(certPath, certPassphrase)];
35 | }
36 |
37 | var connectionFactory = new ConnectionFactory("rabbitmq-transport", connectionConfiguration, certificateCollection, disableCertificateValidation, useExternalAuth, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(10), []);
38 | var brokerConnection = new BrokerConnection(brokerVerifier, connectionFactory);
39 |
40 | return brokerConnection;
41 | }
42 |
43 | static string GetConnectionString(string? connectionString, string? connectionStringEnv)
44 | {
45 | if (string.IsNullOrWhiteSpace(connectionString))
46 | {
47 | var environment = Environment.GetEnvironmentVariable(connectionStringEnv ?? string.Empty);
48 |
49 | if (environment != null)
50 | {
51 | return environment;
52 | }
53 | }
54 |
55 | return connectionString ?? string.Empty;
56 | }
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/QuorumQueues/When_immediate_retries_with_quorum_queues.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests
2 | {
3 | using System;
4 | using System.Linq;
5 | using System.Threading.Tasks;
6 | using AcceptanceTesting;
7 | using NServiceBus.AcceptanceTests;
8 | using NUnit.Framework;
9 |
10 | public class When_immediate_retries_with_quorum_queues : NServiceBusAcceptanceTest
11 | {
12 | [Test]
13 | public async Task Should_do_the_configured_number_of_retries()
14 | {
15 | var context = await Scenario.Define()
16 | .WithEndpoint(b => b
17 | .When((session, c) => session.SendLocal(new MessageToBeRetried()))
18 | .DoNotFailOnErrorMessages())
19 | .Done(c => c.ForwardedToErrorQueue)
20 | .Run();
21 |
22 | Assert.Multiple(() =>
23 | {
24 | Assert.That(context.ForwardedToErrorQueue, Is.True);
25 | Assert.That(context.NumberOfTimesInvoked, Is.EqualTo(numberOfRetries + 1), "Message should be retried 5 times immediately");
26 | Assert.That(context.Logs.Count(l => l.Message
27 | .StartsWith($"Immediate Retry is going to retry message '{context.MessageId}' because of an exception:")), Is.EqualTo(numberOfRetries));
28 | });
29 | }
30 |
31 | const int numberOfRetries = 5;
32 |
33 | class Context : ScenarioContext
34 | {
35 | public int NumberOfTimesInvoked { get; set; }
36 |
37 | public bool ForwardedToErrorQueue { get; set; }
38 |
39 | public string MessageId { get; set; }
40 | }
41 |
42 | public class RetryEndpoint : EndpointConfigurationBuilder
43 | {
44 | public RetryEndpoint()
45 | {
46 | EndpointSetup((config, context) =>
47 | {
48 | var scenarioContext = (Context)context.ScenarioContext;
49 | config.Recoverability().Failed(f => f.OnMessageSentToErrorQueue((message, _) =>
50 | {
51 | scenarioContext.ForwardedToErrorQueue = true;
52 | return Task.FromResult(0);
53 | }));
54 |
55 | var recoverability = config.Recoverability();
56 | recoverability.Immediate(immediate => immediate.NumberOfRetries(numberOfRetries));
57 |
58 | config.SendFailedMessagesTo("error-quorum");
59 | });
60 | }
61 |
62 | class MessageToBeRetriedHandler : IHandleMessages
63 | {
64 | public MessageToBeRetriedHandler(Context context)
65 | {
66 | testContext = context;
67 | }
68 |
69 | public Task Handle(MessageToBeRetried message, IMessageHandlerContext context)
70 | {
71 | testContext.MessageId = context.MessageId;
72 | testContext.NumberOfTimesInvoked++;
73 |
74 | throw new SimulatedException();
75 | }
76 |
77 | Context testContext;
78 | }
79 | }
80 |
81 | public class MessageToBeRetried : IMessage
82 | {
83 | public Guid Id { get; set; }
84 | }
85 | }
86 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/When_publishing_message_implementing_interface_in_direct_topology.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests
2 | {
3 | using System.Linq;
4 | using System.Threading.Tasks;
5 | using AcceptanceTesting;
6 | using Features;
7 | using Logging;
8 | using NServiceBus.AcceptanceTests;
9 | using NServiceBus.AcceptanceTests.EndpointTemplates;
10 | using NUnit.Framework;
11 |
12 | public class When_publishing_message_implementing_interface_in_direct_topology : NServiceBusAcceptanceTest
13 | {
14 | [Test]
15 | public async Task Should_log_a_warning()
16 | {
17 | var context = await Scenario.Define()
18 | .WithEndpoint(b =>
19 | b.When(c => c.Subscriber1Subscribed, session => session.Publish(new MyRequest())))
20 | .WithEndpoint(b => b.When(async (session, c) =>
21 | {
22 | await session.Subscribe();
23 |
24 | if (c.HasNativePubSubSupport)
25 | {
26 | c.Subscriber1Subscribed = true;
27 | }
28 | }))
29 | .Done(c => c.GotTheMessage)
30 | .Run();
31 |
32 | Assert.That(context.Logs.Any(l => l.Level == LogLevel.Warn && l.Message.Contains("The direct routing topology cannot properly publish a message type that implements")), Is.True);
33 | }
34 |
35 | public class Publisher : EndpointConfigurationBuilder
36 | {
37 | public Publisher()
38 | {
39 | EndpointSetup(c =>
40 | {
41 | c.ConfigureRabbitMQTransport().RoutingTopology = new DirectRoutingTopology(true, QueueType.Classic);
42 | });
43 | }
44 | }
45 |
46 | public class Receiver : EndpointConfigurationBuilder
47 | {
48 | public Receiver()
49 | {
50 | EndpointSetup(builder =>
51 | {
52 | builder.ConfigureRabbitMQTransport().RoutingTopology = new DirectRoutingTopology(true, QueueType.Classic);
53 | builder.DisableFeature();
54 | }, metadata => metadata.RegisterPublisherFor(typeof(Publisher)));
55 | }
56 |
57 | class MyEventHandler : IHandleMessages
58 | {
59 | readonly MyContext myContext;
60 |
61 | public MyEventHandler(MyContext myContext)
62 | {
63 | this.myContext = myContext;
64 | }
65 |
66 | public Task Handle(IMyRequest message, IMessageHandlerContext context)
67 | {
68 | myContext.GotTheMessage = true;
69 |
70 | return Task.CompletedTask;
71 | }
72 | }
73 | }
74 |
75 | public class MyRequest : IMyRequest, IOtherRequest
76 | {
77 | }
78 |
79 | public interface IMyRequest : IEvent
80 | {
81 | }
82 |
83 | public interface IOtherRequest : IEvent
84 | {
85 | }
86 |
87 | class MyContext : ScenarioContext
88 | {
89 | public bool GotTheMessage { get; set; }
90 | public bool Subscriber1Subscribed { get; set; }
91 | }
92 | }
93 | }
--------------------------------------------------------------------------------
/src/targets/Broker.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using System.Net.Http;
4 | using System.Text;
5 | using NServiceBus.Transport.RabbitMQ;
6 |
7 | class Broker
8 | {
9 | public static void DeleteVirtualHost()
10 | {
11 | try
12 | {
13 | Send(GetBroker().CreateVirtualHostRequest(HttpMethod.Delete));
14 | }
15 | catch (HttpRequestException ex) when (ex?.StatusCode == HttpStatusCode.NotFound)
16 | {
17 | }
18 | }
19 |
20 | public static void CreateVirtualHost() => Send(GetBroker().CreateVirtualHostRequest(HttpMethod.Put));
21 |
22 | public static void AddUserToVirtualHost() => Send(GetBroker().CreateUserPermissionRequest(HttpMethod.Put));
23 |
24 | static void Send(HttpRequestMessage request)
25 | {
26 | using (var httpClient = new HttpClient())
27 | {
28 | httpClient.Send(request);
29 | }
30 | }
31 |
32 | public static Broker GetBroker()
33 | {
34 | var connectionString = Environment.GetEnvironmentVariable("RabbitMQTransport_ConnectionString") ?? "host=localhost";
35 |
36 | var connectionConfiguration = ConnectionConfiguration.Create(connectionString);
37 |
38 | string hostName = connectionConfiguration.Host;
39 | string username = connectionConfiguration.UserName ?? "guest";
40 | string password = connectionConfiguration.Password ?? "guest";
41 | string virtualHost = connectionConfiguration.VirtualHost ?? "/";
42 | int port = connectionConfiguration.UseTls ? 443 : 15672;
43 |
44 | return new Broker
45 | {
46 | UserName = username,
47 | Password = password,
48 | VirtualHost = virtualHost,
49 | HostName = hostName,
50 | Port = port,
51 | };
52 | }
53 |
54 | public HttpRequestMessage CreateVirtualHostRequest(HttpMethod method) =>
55 | CreateHttpWebRequest($"http{(Port == 443 ? "s" : string.Empty)}://{HostName}:{Port}/api/vhosts/{Uri.EscapeDataString(VirtualHost)}", method);
56 |
57 | public HttpRequestMessage CreateUserPermissionRequest(HttpMethod method)
58 | {
59 | var uriString = $"http{(Port == 443 ? "s" : string.Empty)}://{HostName}:{Port}/api/permissions/{Uri.EscapeDataString(VirtualHost)}/{Uri.EscapeDataString(UserName)}";
60 |
61 | var request = CreateHttpWebRequest(uriString, method);
62 |
63 | var bodyString =
64 | @"{
65 | ""scope"" : ""client"",
66 | ""configure"" : "".*"",
67 | ""write"" : "".*"",
68 | ""read"" : "".*""
69 | }";
70 |
71 | var bodyBytes = new ASCIIEncoding().GetBytes(bodyString);
72 |
73 | request.Content = new ByteArrayContent(bodyBytes);
74 | request.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");
75 |
76 | return request;
77 | }
78 |
79 | public HttpRequestMessage CreateHttpWebRequest(string uriString, HttpMethod method)
80 | {
81 | var request = new HttpRequestMessage(method, uriString);
82 |
83 | var encoded = Convert.ToBase64String(Encoding.GetEncoding("ISO-8859-1").GetBytes(UserName + ":" + Password));
84 | request.Headers.Add("Authorization", "Basic " + encoded);
85 |
86 | return request;
87 | }
88 | public string HostName { get; set; }
89 |
90 | public int Port { get; set; }
91 |
92 | public string VirtualHost { get; set; }
93 |
94 | public string UserName { get; set; }
95 |
96 | public string Password { get; set; }
97 | }
98 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/QuorumQueues/When_starting_endpoint_using_quorum_queues.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests
2 | {
3 | using System.Threading.Tasks;
4 | using AcceptanceTesting;
5 | using AcceptanceTesting.Customization;
6 | using Features;
7 | using global::RabbitMQ.Client.Exceptions;
8 | using NServiceBus.AcceptanceTests;
9 | using NUnit.Framework;
10 |
11 | public class When_starting_endpoint_using_quorum_queues : NServiceBusAcceptanceTest
12 | {
13 | [Test]
14 | public async Task Should_create_receiving_queues_as_quorum_queues()
15 | {
16 | var endpointInputQueue = Conventions.EndpointNamingConvention(typeof(QuorumQueueEndpoint));
17 |
18 | using (var connection = await ConnectionHelper.ConnectionFactory.CreateConnectionAsync())
19 | using (var channel = await connection.CreateChannelAsync())
20 | {
21 | await channel.QueueDeleteAsync(endpointInputQueue, false, false);
22 | await channel.QueueDeleteAsync(endpointInputQueue + "-disc", false, false);
23 | await channel.QueueDeleteAsync("QuorumQueueSatelliteReceiver", false, false);
24 | }
25 |
26 | await Scenario.Define()
27 | .WithEndpoint()
28 | .Done(c => c.EndpointsStarted)
29 | .Run();
30 |
31 | // try to declare the same queue as a non-quorum queue, which should fail:
32 | using (var connection = await ConnectionHelper.ConnectionFactory.CreateConnectionAsync())
33 | {
34 | using (var channel = await connection.CreateChannelAsync())
35 | {
36 | var mainQueueException = Assert.CatchAsync(async () => await channel.DeclareClassicQueue(endpointInputQueue));
37 | Assert.That(mainQueueException.Message, Does.Contain("PRECONDITION_FAILED - inequivalent arg 'x-queue-type'"));
38 | }
39 |
40 | using (var channel = await connection.CreateChannelAsync())
41 | {
42 | var instanceSpecificQueueException = Assert.CatchAsync(async () => await channel.DeclareClassicQueue(endpointInputQueue + "-disc"));
43 | Assert.That(instanceSpecificQueueException.Message, Does.Contain("PRECONDITION_FAILED - inequivalent arg 'x-queue-type'"));
44 | }
45 |
46 | using (var channel = await connection.CreateChannelAsync())
47 | {
48 | var satelliteReceiver = Assert.CatchAsync(async () => await channel.DeclareClassicQueue("QuorumQueueSatelliteReceiver"));
49 | Assert.That(satelliteReceiver.Message, Does.Contain("PRECONDITION_FAILED - inequivalent arg 'x-queue-type'"));
50 | }
51 | }
52 | }
53 |
54 | public class QuorumQueueEndpoint : EndpointConfigurationBuilder
55 | {
56 | public QuorumQueueEndpoint()
57 | {
58 | EndpointSetup(config =>
59 | {
60 | config.SendFailedMessagesTo("error-quorum");
61 | config.MakeInstanceUniquelyAddressable("disc");
62 | config.EnableFeature();
63 | });
64 | }
65 |
66 | public class SatelliteFeature : Feature
67 | {
68 | protected override void Setup(FeatureConfigurationContext context)
69 | {
70 | context.AddSatelliteReceiver("QuorumQueueSatelliteReceiver", new QueueAddress("QuorumQueueSatelliteReceiver"), PushRuntimeSettings.Default, (_, __) => RecoverabilityAction.Discard(string.Empty), (_, __, ___) => Task.CompletedTask);
71 | }
72 | }
73 | }
74 | }
75 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/When_the_message_contains_a_legacy_callback_header.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests
2 | {
3 | using System;
4 | using System.Threading.Tasks;
5 | using AcceptanceTesting;
6 | using AcceptanceTesting.Customization;
7 | using NServiceBus.AcceptanceTests;
8 | using NServiceBus.AcceptanceTests.EndpointTemplates;
9 | using NUnit.Framework;
10 |
11 | public class When_the_message_contains_a_legacy_callback_header : NServiceBusAcceptanceTest
12 | {
13 | [Test]
14 | public async Task It_should_reply_to_an_address_sent_in_that_header()
15 | {
16 | var context = await Scenario.Define()
17 | .WithEndpoint(c => c.When(bus =>
18 | {
19 | var options = new SendOptions();
20 | options.SetHeader("NServiceBus.RabbitMQ.CallbackQueue", Conventions.EndpointNamingConvention(typeof(SpyEndpoint)));
21 | return bus.Send(new Request(), options);
22 | }))
23 | .WithEndpoint(b => b.DoNotFailOnErrorMessages())
24 | .WithEndpoint()
25 | .Done(c => c.Done)
26 | .Run(TimeSpan.FromMinutes(1));
27 |
28 | Assert.That(context.RepliedToWrongQueue, Is.False);
29 | }
30 |
31 | public class Request : IMessage
32 | {
33 | }
34 |
35 | public class Reply : IMessage
36 | {
37 | }
38 |
39 | class MyContext : ScenarioContext
40 | {
41 | public bool Done { get; set; }
42 | public bool RepliedToWrongQueue { get; set; }
43 | }
44 |
45 | class OriginatingEndpoint : EndpointConfigurationBuilder
46 | {
47 | public OriginatingEndpoint()
48 | {
49 | EndpointSetup(config =>
50 | config.ConfigureRouting().RouteToEndpoint(typeof(Request), typeof(ReceivingEndpoint)));
51 | }
52 |
53 | class ReplyHandler : IHandleMessages
54 | {
55 | MyContext testContext;
56 |
57 | public ReplyHandler(MyContext testContext)
58 | {
59 | this.testContext = testContext;
60 | }
61 |
62 | public Task Handle(Reply message, IMessageHandlerContext context)
63 | {
64 | testContext.RepliedToWrongQueue = true;
65 | testContext.Done = true;
66 | return Task.CompletedTask;
67 | }
68 | }
69 | }
70 |
71 | class SpyEndpoint : EndpointConfigurationBuilder
72 | {
73 | public SpyEndpoint()
74 | {
75 | EndpointSetup();
76 | }
77 |
78 | class ReplyHandler : IHandleMessages
79 | {
80 | MyContext testContext;
81 |
82 | public ReplyHandler(MyContext testContext)
83 | {
84 | this.testContext = testContext;
85 | }
86 |
87 | public Task Handle(Reply message, IMessageHandlerContext context)
88 | {
89 | testContext.Done = true;
90 | return Task.CompletedTask;
91 | }
92 | }
93 | }
94 |
95 | class ReceivingEndpoint : EndpointConfigurationBuilder
96 | {
97 | public ReceivingEndpoint()
98 | {
99 | EndpointSetup(c =>
100 | {
101 | });
102 | }
103 |
104 | public class RequestHandler : IHandleMessages
105 | {
106 | public Task Handle(Request message, IMessageHandlerContext context)
107 | {
108 | return context.Reply(new Reply());
109 | }
110 | }
111 | }
112 | }
113 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.Tests/Connection/ConnectionConfigurationWithAmqpTests.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.Tests.ConnectionString
2 | {
3 | using System;
4 | using NUnit.Framework;
5 | using RabbitMQ;
6 |
7 | [TestFixture]
8 | public class ConnectionConfigurationWithAmqpTests
9 | {
10 | ConnectionConfiguration defaults = ConnectionConfiguration.Create("amqp://guest:guest@localhost:5672/");
11 |
12 | [Test]
13 | public void Should_correctly_parse_full_connection_string()
14 | {
15 | const string connectionString = "amqp://Copa:abc_xyz@192.168.1.1:5672/Copa";
16 |
17 | var connectionConfiguration = ConnectionConfiguration.Create(connectionString);
18 |
19 | Assert.Multiple(() =>
20 | {
21 | Assert.That(connectionConfiguration.Host, Is.EqualTo("192.168.1.1"));
22 | Assert.That(connectionConfiguration.Port, Is.EqualTo(5672));
23 | Assert.That(connectionConfiguration.VirtualHost, Is.EqualTo("Copa"));
24 | Assert.That(connectionConfiguration.UserName, Is.EqualTo("Copa"));
25 | Assert.That(connectionConfiguration.Password, Is.EqualTo("abc_xyz"));
26 | });
27 | }
28 |
29 | [Test]
30 | public void Should_fail_if_host_is_not_present()
31 | {
32 | Assert.Throws(() => ConnectionConfiguration.Create("amqp://:1234/"));
33 | }
34 |
35 | [TestCase("amqp", 5672U, false)]
36 | [TestCase("amqps", 5671U, true)]
37 | public void Should_determine_if_tls_should_be_used_from_connection_string(string scheme, uint port, bool useTls)
38 | {
39 | var connectionConfiguration = ConnectionConfiguration.Create($"{scheme}://guest:guest@localhost/");
40 |
41 | Assert.Multiple(() =>
42 | {
43 | Assert.That(useTls, Is.EqualTo(connectionConfiguration.UseTls));
44 | Assert.That(port, Is.EqualTo(connectionConfiguration.Port));
45 | });
46 | }
47 |
48 | [Test]
49 | public void Should_use_explicit_port_setting_over_scheme_default()
50 | {
51 | var connectionConfiguration = ConnectionConfiguration.Create("amqp://localhost:1234/");
52 | Assert.That(connectionConfiguration.Port, Is.EqualTo(1234));
53 | }
54 |
55 | [Test]
56 | public void Should_parse_host_without_port()
57 | {
58 | var connectionConfiguration = ConnectionConfiguration.Create("amqp://my.host.com/");
59 |
60 | Assert.Multiple(() =>
61 | {
62 | Assert.That(connectionConfiguration.Host, Is.EqualTo("my.host.com"));
63 | Assert.That(connectionConfiguration.Port, Is.EqualTo(5672));
64 | });
65 | }
66 |
67 | [Test]
68 | public void Should_throw_on_invalid_port()
69 | {
70 | var connectionString = "amqp://localhost:notaport/";
71 |
72 | var exception = Assert.Throws(() => ConnectionConfiguration.Create(connectionString));
73 |
74 | Assert.That(exception.Message, Does.Contain("Invalid URI: Invalid port specified."));
75 | }
76 |
77 | [Test]
78 | public void Should_set_default_port()
79 | {
80 | Assert.That(defaults.Port, Is.EqualTo(5672));
81 | }
82 |
83 | [Test]
84 | public void Should_set_default_virtual_host()
85 | {
86 | Assert.That(defaults.VirtualHost, Is.EqualTo("/"));
87 | }
88 |
89 | [Test]
90 | public void Should_set_default_username()
91 | {
92 | Assert.That(defaults.UserName, Is.EqualTo("guest"));
93 | }
94 |
95 | [Test]
96 | public void Should_set_default_password()
97 | {
98 | Assert.That(defaults.Password, Is.EqualTo("guest"));
99 | }
100 |
101 | [Test]
102 | public void Should_set_default_use_tls()
103 | {
104 | Assert.That(defaults.UseTls, Is.EqualTo(false));
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.TransportTests/When_changing_concurrency.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.TransportTests
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using NServiceBus.TransportTests;
8 | using NUnit.Framework;
9 |
10 | public class When_changing_concurrency : NServiceBusTransportTest
11 | {
12 | [TestCase(TransportTransactionMode.None)]
13 | [TestCase(TransportTransactionMode.ReceiveOnly)]
14 | [TestCase(TransportTransactionMode.SendsAtomicWithReceive)]
15 | [TestCase(TransportTransactionMode.TransactionScope)]
16 | public async Task Should_complete_current_message(TransportTransactionMode transactionMode)
17 | {
18 | var triggeredChangeConcurrency = CreateTaskCompletionSource();
19 | var sentMessageReceived = CreateTaskCompletionSource();
20 | Task concurrencyChanged = null;
21 | int invocationCounter = 0;
22 |
23 | await StartPump(async (context, ct) =>
24 | {
25 | Interlocked.Increment(ref invocationCounter);
26 |
27 | concurrencyChanged = Task.Run(async () =>
28 | {
29 | var task = receiver.ChangeConcurrency(new PushRuntimeSettings(1), ct);
30 | triggeredChangeConcurrency.SetResult();
31 | await task;
32 | }, ct);
33 |
34 | sentMessageReceived.SetResult();
35 | await triggeredChangeConcurrency.Task;
36 |
37 | }, (_, _) =>
38 | {
39 | Assert.Fail("Message processing should not fail");
40 | return Task.FromResult(ErrorHandleResult.RetryRequired);
41 | },
42 | transactionMode);
43 |
44 | await SendMessage(InputQueueName);
45 | await sentMessageReceived.Task;
46 | await concurrencyChanged;
47 | await StopPump();
48 |
49 | Assert.That(invocationCounter, Is.EqualTo(1), "message should successfully complete on first processing attempt");
50 | }
51 |
52 | [TestCase(TransportTransactionMode.None)]
53 | [TestCase(TransportTransactionMode.ReceiveOnly)]
54 | [TestCase(TransportTransactionMode.SendsAtomicWithReceive)]
55 | [TestCase(TransportTransactionMode.TransactionScope)]
56 | public async Task Should_dispatch_messages_from_on_error(TransportTransactionMode transactionMode)
57 | {
58 | int invocationCounter = 0;
59 |
60 | var triggeredChangeConcurrency = CreateTaskCompletionSource();
61 | var sentMessageReceived = CreateTaskCompletionSource();
62 |
63 | await StartPump((context, _) =>
64 | {
65 | Interlocked.Increment(ref invocationCounter);
66 | if (context.Headers.TryGetValue("FromOnError", out var value) && value == bool.TrueString)
67 | {
68 | sentMessageReceived.SetResult();
69 | return Task.CompletedTask;
70 | }
71 |
72 | throw new Exception("triggering recoverability pipeline");
73 | }, async (context, ct) =>
74 | {
75 | // same behavior as delayed retries
76 | _ = Task.Run(async () =>
77 | {
78 | var task = receiver.ChangeConcurrency(new PushRuntimeSettings(1), ct);
79 | triggeredChangeConcurrency.SetResult();
80 | await task;
81 | }, ct);
82 |
83 | await triggeredChangeConcurrency.Task;
84 | await SendMessage(InputQueueName,
85 | new Dictionary() { { "FromOnError", bool.TrueString } },
86 | context.TransportTransaction, cancellationToken: ct);
87 | return ErrorHandleResult.Handled;
88 | },
89 | transactionMode);
90 |
91 | await SendMessage(InputQueueName);
92 | await sentMessageReceived.Task;
93 | await StopPump();
94 |
95 | Assert.That(invocationCounter, Is.EqualTo(2), "there should be exactly 2 messages (initial message and new message from onError pipeline)");
96 | }
97 | }
98 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Sending/MessageDispatcher.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Diagnostics.CodeAnalysis;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using global::RabbitMQ.Client;
9 |
10 | class MessageDispatcher : IMessageDispatcher
11 | {
12 | readonly ChannelProvider channelProvider;
13 | readonly Action messageCustomization;
14 | readonly bool supportsDelayedDelivery;
15 |
16 | public MessageDispatcher(
17 | ChannelProvider channelProvider,
18 | Action messageCustomization,
19 | bool supportsDelayedDelivery
20 | )
21 | {
22 | this.channelProvider = channelProvider;
23 | this.messageCustomization = messageCustomization ?? (static (_, _) => { });
24 | this.supportsDelayedDelivery = supportsDelayedDelivery;
25 | }
26 |
27 | public async Task Dispatch(TransportOperations outgoingMessages, TransportTransaction transaction, CancellationToken cancellationToken = default)
28 | {
29 | var channel = await channelProvider.GetPublishChannel(cancellationToken).ConfigureAwait(false);
30 |
31 | try
32 | {
33 | var unicastTransportOperations = outgoingMessages.UnicastTransportOperations;
34 | var multicastTransportOperations = outgoingMessages.MulticastTransportOperations;
35 |
36 | var tasks = new List(unicastTransportOperations.Count + multicastTransportOperations.Count);
37 |
38 | foreach (var operation in unicastTransportOperations)
39 | {
40 | tasks.Add(SendMessage(operation, channel, cancellationToken).AsTask());
41 | }
42 |
43 | foreach (var operation in multicastTransportOperations)
44 | {
45 | tasks.Add(PublishMessage(operation, channel, cancellationToken).AsTask());
46 | }
47 |
48 | await Task.WhenAll(tasks).ConfigureAwait(false);
49 | }
50 | finally
51 | {
52 | await channelProvider.ReturnPublishChannel(channel, cancellationToken)
53 | .ConfigureAwait(false);
54 | }
55 | }
56 |
57 | ValueTask SendMessage(UnicastTransportOperation transportOperation, ConfirmsAwareChannel channel, CancellationToken cancellationToken)
58 | {
59 | ThrowIfDelayedDeliveryIsDisabledAndMessageIsDelayed(transportOperation);
60 |
61 | var message = transportOperation.Message;
62 |
63 | var properties = new BasicProperties();
64 | properties.Fill(message, transportOperation.Properties);
65 | messageCustomization(transportOperation, properties);
66 |
67 | return channel.SendMessage(transportOperation.Destination, message, properties, cancellationToken);
68 | }
69 |
70 | ValueTask PublishMessage(MulticastTransportOperation transportOperation, ConfirmsAwareChannel channel, CancellationToken cancellationToken)
71 | {
72 | ThrowIfDelayedDeliveryIsDisabledAndMessageIsDelayed(transportOperation);
73 |
74 | var message = transportOperation.Message;
75 |
76 | var properties = new BasicProperties();
77 | properties.Fill(message, transportOperation.Properties);
78 | messageCustomization(transportOperation, properties);
79 |
80 | return channel.PublishMessage(transportOperation.MessageType, message, properties, cancellationToken);
81 | }
82 |
83 | void ThrowIfDelayedDeliveryIsDisabledAndMessageIsDelayed(IOutgoingTransportOperation transportOperation)
84 | {
85 | if (supportsDelayedDelivery)
86 | {
87 | return;
88 | }
89 |
90 | if (transportOperation.Properties.DelayDeliveryWith != null || transportOperation.Properties.DoNotDeliverBefore != null)
91 | {
92 | ThrowDelayedDeliveryDisabled();
93 | }
94 | }
95 |
96 | [DoesNotReturn]
97 | static void ThrowDelayedDeliveryDisabled() => throw new Exception("Delayed delivery has been disabled in the transport settings.");
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Configuration/ManagementApiConfiguration.cs:
--------------------------------------------------------------------------------
1 | #nullable enable
2 |
3 | namespace NServiceBus
4 | {
5 | using System;
6 | using System.Runtime.CompilerServices;
7 |
8 | ///
9 | /// The RabbitMQ management API configuration to use instead of inferring values from the connection string.
10 | ///
11 | public class ManagementApiConfiguration
12 | {
13 | ManagementApiConfiguration() { }
14 |
15 | ///
16 | /// Initializes a new instance of the class.
17 | ///
18 | /// The URL to use when connecting to the RabbitMQ management API.
19 | public ManagementApiConfiguration(string url)
20 | {
21 | ArgumentException.ThrowIfNullOrWhiteSpace(url);
22 | ThrowIfNotValidUrl(url);
23 |
24 | Url = url;
25 | }
26 |
27 | ///
28 | /// Initializes a new instance of the class.
29 | ///
30 | /// The user name to use when connecting to the RabbitMQ management API.
31 | /// The password to use when connecting to the RabbitMQ management API.
32 | public ManagementApiConfiguration(string userName, string password)
33 | {
34 | ArgumentException.ThrowIfNullOrWhiteSpace(userName);
35 | ArgumentException.ThrowIfNullOrWhiteSpace(password);
36 |
37 | UserName = userName;
38 | Password = password;
39 | }
40 |
41 | ///
42 | /// Initializes a new instance of the class.
43 | ///
44 | /// The URL to use when connecting to the RabbitMQ management API.
45 | /// The user name to use when connecting to the RabbitMQ management API.
46 | /// The password to use when connecting to the RabbitMQ management API.
47 | public ManagementApiConfiguration(string url, string userName, string password)
48 | {
49 | ArgumentException.ThrowIfNullOrWhiteSpace(url);
50 | ThrowIfNotValidUrl(url);
51 | ArgumentException.ThrowIfNullOrWhiteSpace(userName);
52 | ArgumentException.ThrowIfNullOrWhiteSpace(password);
53 |
54 | Url = url;
55 | UserName = userName;
56 | Password = password;
57 | }
58 |
59 | ///
60 | /// The URL to use when connecting to the RabbitMQ management API.
61 | ///
62 | public string? Url { get; private init; }
63 |
64 | ///
65 | /// The user name to use when connecting to the RabbitMQ management API.
66 | ///
67 | public string? UserName { get; private init; }
68 |
69 | ///
70 | /// The password to use when connecting to the RabbitMQ management API.
71 | ///
72 | public string? Password { get; private init; }
73 |
74 | internal static ManagementApiConfiguration Create(string? url, string? userName, string? password)
75 | {
76 | ThrowIfWhiteSpace(url);
77 |
78 | if (url is not null)
79 | {
80 | ThrowIfNotValidUrl(url);
81 | }
82 |
83 | ThrowIfWhiteSpace(userName);
84 | ThrowIfWhiteSpace(password);
85 |
86 | return new ManagementApiConfiguration
87 | {
88 | Url = url,
89 | UserName = userName,
90 | Password = password
91 | };
92 | }
93 |
94 | static void ThrowIfWhiteSpace(string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
95 | {
96 | if (argument is not null && string.IsNullOrWhiteSpace(argument))
97 | {
98 | throw new ArgumentException("The value cannot be an empty string or composed entirely of whitespace.", paramName);
99 | }
100 | }
101 |
102 | static void ThrowIfNotValidUrl(string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
103 | {
104 | if (!Uri.IsWellFormedUriString(argument, UriKind.RelativeOrAbsolute))
105 | {
106 | throw new ArgumentException("The value is not a valid URL.", paramName);
107 | }
108 | }
109 |
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/When_receiving_a_reply_that_contains_a_legacy_callback_header.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests
2 | {
3 | using System.Threading.Tasks;
4 | using AcceptanceTesting;
5 | using AcceptanceTesting.Customization;
6 | using NServiceBus.AcceptanceTests;
7 | using NServiceBus.AcceptanceTests.EndpointTemplates;
8 | using NUnit.Framework;
9 |
10 | public class When_receiving_a_reply_that_contains_a_legacy_callback_header : NServiceBusAcceptanceTest
11 | {
12 | [Test]
13 | public async Task The_audit_message_should_go_to_the_correct_queue()
14 | {
15 | var context = await Scenario.Define()
16 | .WithEndpoint(c => c.When(bus =>
17 | {
18 | return bus.Send(new Request());
19 | }))
20 | .WithEndpoint()
21 | .WithEndpoint()
22 | .Done(c => c.IncorrectHandlerInvoked || c.AuditMessageReceived)
23 | .Run();
24 |
25 | Assert.Multiple(() =>
26 | {
27 | Assert.That(context.IncorrectHandlerInvoked, Is.False);
28 | Assert.That(context.AuditMessageReceived, Is.True);
29 | });
30 | }
31 |
32 | public class Request : IMessage
33 | {
34 | }
35 |
36 | public class Reply : IMessage
37 | {
38 | }
39 |
40 | class MyContext : ScenarioContext
41 | {
42 | public bool IncorrectHandlerInvoked { get; set; }
43 |
44 | public bool AuditMessageReceived { get; set; }
45 |
46 | }
47 |
48 | class OriginatingEndpoint : EndpointConfigurationBuilder
49 | {
50 | public OriginatingEndpoint()
51 | {
52 | EndpointSetup(config =>
53 | {
54 | config.ConfigureRouting().RouteToEndpoint(typeof(Request), typeof(ReceivingEndpoint));
55 | config.AuditProcessedMessagesTo();
56 | });
57 | }
58 |
59 | class ReplyHandler : IHandleMessages
60 | {
61 | public Task Handle(Reply message, IMessageHandlerContext context)
62 | {
63 | return Task.CompletedTask;
64 | }
65 | }
66 | }
67 |
68 | class ReceivingEndpoint : EndpointConfigurationBuilder
69 | {
70 | public ReceivingEndpoint()
71 | {
72 | EndpointSetup();
73 | }
74 |
75 | public class RequestHandler : IHandleMessages
76 | {
77 | public Task Handle(Request message, IMessageHandlerContext context)
78 | {
79 | var options = new ReplyOptions();
80 | options.SetHeader("NServiceBus.RabbitMQ.CallbackQueue", Conventions.EndpointNamingConvention(typeof(ReceivingEndpoint)));
81 |
82 | return context.Reply(new Reply(), options);
83 | }
84 | }
85 |
86 | class ReplyHandler : IHandleMessages
87 | {
88 | MyContext testContext;
89 |
90 | public ReplyHandler(MyContext testContext)
91 | {
92 | this.testContext = testContext;
93 | }
94 |
95 | public Task Handle(Reply message, IMessageHandlerContext context)
96 | {
97 | testContext.IncorrectHandlerInvoked = true;
98 |
99 | return Task.CompletedTask;
100 | }
101 | }
102 | }
103 |
104 | class AuditSpyEndpoint : EndpointConfigurationBuilder
105 | {
106 | public AuditSpyEndpoint()
107 | {
108 | EndpointSetup();
109 | }
110 |
111 | class ReplyHandler : IHandleMessages
112 | {
113 | MyContext testContext;
114 |
115 | public ReplyHandler(MyContext testContext)
116 | {
117 | this.testContext = testContext;
118 | }
119 |
120 | public Task Handle(Reply message, IMessageHandlerContext context)
121 | {
122 | testContext.AuditMessageReceived = true;
123 |
124 | return Task.CompletedTask;
125 | }
126 | }
127 | }
128 | }
129 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NServiceBus.RabbitMQ
2 |
3 | NServiceBus.RabbitMQ supports sending messages over [RabbitMQ](http://www.rabbitmq.com/) using the [RabbitMQ .NET Client](https://www.nuget.org/packages/RabbitMQ.Client/).
4 |
5 | It is part of the [Particular Service Platform](https://particular.net/service-platform), which includes [NServiceBus](https://particular.net/nservicebus) and tools to build, monitor, and debug distributed systems.
6 |
7 | See the [RabbitMQ Transport documentation](https://docs.particular.net/transports/rabbitmq/) for more details on how to use it.
8 |
9 | ## Running tests locally
10 |
11 | All tests use the default connection string `host=localhost`. This can be changed by setting the `RabbitMQTransport_ConnectionString` environment variable.
12 |
13 | For developers using Docker containers, the following docker command will quickly setup a container configured to use the default port:
14 |
15 | `docker run -d --hostname my-rabbit --name my-rabbit -p 5672:5672 -p 15672:15672 rabbitmq:4-management`
16 |
17 | With this setup, the default connection string will work.
18 |
19 | ## Setting up a docker cluster
20 |
21 | A 3-node RabbitMQ cluster with a HAProxy load balancer in front and by default mirroring all queues across all 3 nodes can be set up using docker for testing or development purposes by running the following script:
22 |
23 | Setup cluster network:
24 |
25 | ```cmd
26 | docker network create --driver bridge rabbitnet
27 | ```
28 |
29 | Setup cluster:
30 |
31 | ```cmd
32 | docker run -d --network rabbitnet --hostname rabbit1 --name rabbit1 -v rabbitmq-data:/var/lib/rabbitmq rabbitmq:3-management
33 | docker run -d --network rabbitnet --hostname rabbit2 --name rabbit2 -v rabbitmq-data:/var/lib/rabbitmq rabbitmq:3-management
34 | docker run -d --network rabbitnet --hostname rabbit3 --name rabbit3 -v rabbitmq-data:/var/lib/rabbitmq rabbitmq:3-management
35 |
36 | docker exec rabbit2 rabbitmqctl stop_app
37 | docker exec rabbit2 rabbitmqctl join_cluster rabbit@rabbit1
38 | docker exec rabbit2 rabbitmqctl start_app
39 |
40 | docker exec rabbit3 rabbitmqctl stop_app
41 | docker exec rabbit3 rabbitmqctl join_cluster rabbit@rabbit1
42 | docker exec rabbit3 rabbitmqctl start_app
43 | ```
44 |
45 | Setup classic queue mirroring:
46 |
47 | Note that [mirroring of classic queues](https://www.rabbitmq.com/ha.html) will be removed in a future version of RabbitMQ. Consider using [quorum queues](https://www.rabbitmq.com/quorum-queues.html) instead.
48 |
49 | ```cmd
50 | docker exec rabbit1 rabbitmqctl set_policy ha-all "\." '{"ha-mode":"exactly","ha-params":2,"ha-sync-mode":"automatic"}'
51 | ```
52 |
53 | Create `haproxy.cfg` file for configuring HAProxy:
54 |
55 | ```txt
56 | global
57 | log 127.0.0.1 local1
58 | maxconn 4096
59 |
60 | defaults
61 | log global
62 | mode tcp
63 | option tcplog
64 | retries 3
65 | option redispatch
66 | maxconn 2000
67 | timeout connect 5000
68 | timeout client 50000
69 | timeout server 50000
70 |
71 | listen stats
72 | bind *:1936
73 | mode http
74 | stats enable
75 | stats hide-version
76 | stats realm Haproxy\ Statistics
77 | stats uri /
78 |
79 | listen rabbitmq
80 | bind *:5672
81 | mode tcp
82 | balance roundrobin
83 | timeout client 3h
84 | timeout server 3h
85 | option clitcpka
86 | server rabbit1 rabbit1:5672 check inter 5s rise 2 fall 3
87 | server rabbit2 rabbit2:5672 check inter 5s rise 2 fall 3
88 | server rabbit3 rabbit3:5672 check inter 5s rise 2 fall 3
89 |
90 | listen mgmt
91 | bind *:15672
92 | mode tcp
93 | balance roundrobin
94 | timeout client 3h
95 | timeout server 3h
96 | option clitcpka
97 | server rabbit1 rabbit1:15672 check inter 5s rise 2 fall 3
98 | server rabbit2 rabbit2:15672 check inter 5s rise 2 fall 3
99 | server rabbit3 rabbit3:15672 check inter 5s rise 2 fall 3
100 | ```
101 |
102 | Setup HAProxy container, note correct the path where `haproxy.cfg` is saved.
103 |
104 | ```cmd
105 | docker run -d --network rabbitnet --hostname rabbitha --name rabbitha -p 15672:15672 -p 5672:5672 -v ./haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro haproxy:1.7
106 | ```
107 |
108 | Setup quorum queues:
109 |
110 | [quorum queues](https://www.rabbitmq.com/quorum-queues.html)
111 |
112 | After all these commands have run, a 3-node RabbitMQ cluster will be running that should be accessible via the load balancer.
113 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.TransportTests/When_modifying_incoming_headers.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.TransportTests
2 | {
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Threading.Tasks;
6 | using NServiceBus.TransportTests;
7 | using NUnit.Framework;
8 |
9 | public class When_modifying_incoming_headers : NServiceBusTransportTest
10 | {
11 | [TestCase(TransportTransactionMode.ReceiveOnly)]
12 | [TestCase(TransportTransactionMode.SendsAtomicWithReceive)]
13 | [TestCase(TransportTransactionMode.TransactionScope)]
14 | public async Task Should_roll_back_header_modifications_between_processing_attempts(TransportTransactionMode transactionMode)
15 | {
16 | var messageRetries = new TaskCompletionSource();
17 | var firstInvocation = true;
18 |
19 | await StartPump(
20 | (context, _) =>
21 | {
22 | if (firstInvocation)
23 | {
24 | context.Headers["test-header"] = "modified";
25 | firstInvocation = false;
26 | throw new Exception();
27 | }
28 |
29 | messageRetries.SetResult(context);
30 | return Task.FromResult(0);
31 | },
32 | (_, __) => Task.FromResult(ErrorHandleResult.RetryRequired),
33 | transactionMode);
34 |
35 | await SendMessage(InputQueueName, new Dictionary
36 | {
37 | {"test-header", "original"}
38 | });
39 |
40 | var retriedMessage = await messageRetries.Task;
41 |
42 | Assert.That(retriedMessage.Headers["test-header"], Is.EqualTo("original"));
43 | }
44 |
45 | [TestCase(TransportTransactionMode.None)]
46 | [TestCase(TransportTransactionMode.ReceiveOnly)]
47 | [TestCase(TransportTransactionMode.SendsAtomicWithReceive)]
48 | [TestCase(TransportTransactionMode.TransactionScope)]
49 | public async Task Should_roll_back_header_modifications_before_handling_error(TransportTransactionMode transactionMode)
50 | {
51 | var errorHandled = new TaskCompletionSource();
52 |
53 | await StartPump(
54 | (context, _) =>
55 | {
56 | context.Headers["test-header"] = "modified";
57 | throw new Exception();
58 | },
59 | (context, _) =>
60 | {
61 | errorHandled.SetResult(context);
62 | return Task.FromResult(ErrorHandleResult.Handled);
63 | },
64 | transactionMode);
65 |
66 | await SendMessage(InputQueueName, new Dictionary
67 | {
68 | {"test-header", "original"}
69 | });
70 |
71 | var errorContext = await errorHandled.Task;
72 |
73 | Assert.That(errorContext.Message.Headers["test-header"], Is.EqualTo("original"));
74 | }
75 |
76 | [TestCase(TransportTransactionMode.ReceiveOnly)]
77 | [TestCase(TransportTransactionMode.SendsAtomicWithReceive)]
78 | [TestCase(TransportTransactionMode.TransactionScope)]
79 | public async Task Should_roll_back_header_modifications_made_while_handling_error(TransportTransactionMode transactionMode)
80 | {
81 | var messageRetries = new TaskCompletionSource();
82 | var firstInvocation = true;
83 |
84 | await StartPump(
85 | (context, _) =>
86 | {
87 | if (firstInvocation)
88 | {
89 | firstInvocation = false;
90 | throw new Exception();
91 | }
92 |
93 | messageRetries.SetResult(context);
94 | return Task.FromResult(0);
95 | },
96 | (context, _) =>
97 | {
98 | context.Message.Headers["test-header"] = "modified";
99 | return Task.FromResult(ErrorHandleResult.RetryRequired);
100 | },
101 | transactionMode);
102 |
103 | await SendMessage(InputQueueName, new Dictionary
104 | {
105 | {"test-header", "original"}
106 | });
107 |
108 | var retriedMessage = await messageRetries.Task;
109 |
110 | Assert.That(retriedMessage.Headers["test-header"], Is.EqualTo("original"));
111 | }
112 | }
113 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/When_the_broker_connection_is_lost.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests
2 | {
3 | using System;
4 | using System.Threading;
5 | using System.Threading.Tasks;
6 | using AcceptanceTesting;
7 | using Features;
8 | using Microsoft.Extensions.DependencyInjection;
9 | using NServiceBus.AcceptanceTests;
10 | using NServiceBus.AcceptanceTests.EndpointTemplates;
11 | using NUnit.Framework;
12 | using Performance.TimeToBeReceived;
13 | using Routing;
14 | using Settings;
15 |
16 | public class When_the_broker_connection_is_lost : NServiceBusAcceptanceTest
17 | {
18 | [Test]
19 | public async Task Should_reconnect()
20 | {
21 | var context = await Scenario.Define(ctx =>
22 | {
23 | ctx.MessageId = Guid.NewGuid().ToString();
24 | })
25 | .WithEndpoint()
26 | .Done(c => c.GotTheMessage)
27 | .Run();
28 |
29 | Assert.That(context.GotTheMessage, Is.True, "Should receive the message");
30 | }
31 |
32 | public class Receiver : EndpointConfigurationBuilder
33 | {
34 | public Receiver() =>
35 | EndpointSetup(e =>
36 | {
37 | e.EnableFeature();
38 | });
39 |
40 | class ConnectionKillerFeature : Feature
41 | {
42 | protected override void Setup(FeatureConfigurationContext context)
43 | {
44 | context.Services.AddTransient();
45 | context.RegisterStartupTask(b => b.GetRequiredService());
46 | }
47 |
48 | class ConnectionKiller(IMessageDispatcher sender, IReadOnlySettings settings, MyContext context)
49 | : FeatureStartupTask
50 | {
51 | protected override async Task OnStart(IMessageSession session, CancellationToken cancellationToken = default)
52 | {
53 | await BreakConnectionBySendingInvalidMessage(cancellationToken);
54 |
55 | await session.SendLocal(new MyRequest { MessageId = context.MessageId }, cancellationToken);
56 | }
57 |
58 | protected override Task OnStop(IMessageSession session, CancellationToken cancellationToken = default) => Task.CompletedTask;
59 |
60 | async Task BreakConnectionBySendingInvalidMessage(CancellationToken cancellationToken)
61 | {
62 | try
63 | {
64 | var outgoingMessage = new OutgoingMessage("Foo", [], Array.Empty());
65 | var props = new DispatchProperties
66 | {
67 | DiscardIfNotReceivedBefore =
68 | new DiscardIfNotReceivedBefore(TimeSpan.FromMilliseconds(-1))
69 | };
70 | var operation = new TransportOperation(outgoingMessage, new UnicastAddressTag(settings.EndpointName()), props);
71 | await sender.Dispatch(new TransportOperations(operation), new TransportTransaction(), cancellationToken);
72 | }
73 | catch (Exception ex) when (!ex.IsCausedBy(cancellationToken))
74 | {
75 | // Don't care
76 | }
77 | }
78 | }
79 | }
80 |
81 | class MyHandler : IHandleMessages
82 | {
83 | readonly MyContext myContext;
84 |
85 | public MyHandler(MyContext myContext)
86 | {
87 | this.myContext = myContext;
88 | }
89 |
90 | public Task Handle(MyRequest message, IMessageHandlerContext context)
91 | {
92 | if (message.MessageId == myContext.MessageId)
93 | {
94 | myContext.GotTheMessage = true;
95 | }
96 |
97 | return Task.CompletedTask;
98 | }
99 | }
100 | }
101 |
102 | public class MyRequest : IMessage
103 | {
104 | public string MessageId { get; set; }
105 | }
106 |
107 | class MyContext : ScenarioContext
108 | {
109 | public bool GotTheMessage { get; set; }
110 | public string MessageId { get; set; }
111 | }
112 | }
113 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.Tests/RabbitMqContext.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.Tests
2 | {
3 | using System;
4 | using System.Collections.Concurrent;
5 | using System.Collections.Generic;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using NUnit.Framework;
9 |
10 | abstract class RabbitMqContext
11 | {
12 | public virtual int MaximumConcurrency => 1;
13 |
14 | [SetUp]
15 | public virtual async Task SetUp()
16 | {
17 | receivedMessages = [];
18 |
19 | var connectionString = Environment.GetEnvironmentVariable("RabbitMQTransport_ConnectionString") ?? "host=localhost";
20 |
21 | var useTls = connectionString.StartsWith("https", StringComparison.InvariantCultureIgnoreCase) || connectionString.StartsWith("amqps", StringComparison.InvariantCultureIgnoreCase);
22 |
23 | var transport = new RabbitMQTransport(RoutingTopology.Conventional(queueType), connectionString)
24 | {
25 | // The startup costs for creating a policy for every test queue add up, and the tests shouldn't be impacted by the default delivery limit.
26 | ValidateDeliveryLimits = false
27 | };
28 |
29 | var connectionConfig = transport.ConnectionConfiguration;
30 |
31 | connectionFactory = new ConnectionFactory(ReceiverQueue, connectionConfig, null, true, false, transport.HeartbeatInterval, transport.NetworkRecoveryInterval, null);
32 |
33 | infra = await transport.Initialize(new HostSettings(ReceiverQueue, ReceiverQueue, new StartupDiagnosticEntries(), (_, _, _) => { }, true),
34 | [new ReceiveSettings(ReceiverQueue, new QueueAddress(ReceiverQueue), true, true, ErrorQueue)], [.. AdditionalReceiverQueues, ErrorQueue]);
35 |
36 | messageDispatcher = infra.Dispatcher;
37 | messagePump = infra.Receivers[ReceiverQueue];
38 | subscriptionManager = messagePump.Subscriptions;
39 | OnMessage = (messageContext, cancellationToken) =>
40 | {
41 | receivedMessages.Add(new IncomingMessage(messageContext.NativeMessageId, messageContext.Headers, messageContext.Body), cancellationToken);
42 | return Task.CompletedTask;
43 | };
44 |
45 | OnError = (_, _) => Task.FromResult(ErrorHandleResult.Handled);
46 |
47 | await messagePump.Initialize(
48 | new PushRuntimeSettings(MaximumConcurrency),
49 | (messageContext, cancellationToken) => OnMessage(messageContext, cancellationToken),
50 | (errorContext, cancellationToken) => OnError(errorContext, cancellationToken));
51 |
52 | await messagePump.StartReceive();
53 | }
54 |
55 | [TearDown]
56 | public async Task TearDown()
57 | {
58 | if (messagePump != null)
59 | {
60 | await messagePump.StopReceive();
61 | }
62 |
63 | if (infra != null)
64 | {
65 | await infra.Shutdown();
66 | }
67 | }
68 |
69 | protected bool TryWaitForMessageReceipt() => TryReceiveMessage(out var _, IncomingMessageTimeout);
70 |
71 | protected IncomingMessage ReceiveMessage()
72 | {
73 | if (!TryReceiveMessage(out var message, IncomingMessageTimeout))
74 | {
75 | throw new TimeoutException($"The message did not arrive within {IncomingMessageTimeout.TotalSeconds} seconds.");
76 | }
77 |
78 | return message;
79 | }
80 |
81 | bool TryReceiveMessage(out IncomingMessage message, TimeSpan timeout) =>
82 | receivedMessages.TryTake(out message, timeout);
83 |
84 | protected IList AdditionalReceiverQueues = [];
85 |
86 | protected Func OnMessage;
87 | protected Func> OnError;
88 |
89 | protected string ReceiverQueue => GetTestQueueName("testreceiver");
90 | protected string ErrorQueue => GetTestQueueName("error");
91 |
92 | protected string GetTestQueueName(string queueName) => $"{queueName}-{queueType}";
93 |
94 | protected QueueType queueType = QueueType.Quorum;
95 | protected ConnectionFactory connectionFactory;
96 | protected IMessageDispatcher messageDispatcher;
97 | protected IMessageReceiver messagePump;
98 | protected ISubscriptionManager subscriptionManager;
99 |
100 | BlockingCollection receivedMessages;
101 |
102 | protected static readonly TimeSpan IncomingMessageTimeout = TimeSpan.FromSeconds(5);
103 | TransportInfrastructure infra;
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ.AcceptanceTests/When_using_a_custom_message_id_strategy.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests
2 | {
3 | using System.Collections.Generic;
4 | using System.Text;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using AcceptanceTesting;
8 | using Features;
9 | using Microsoft.Extensions.DependencyInjection;
10 | using NServiceBus.AcceptanceTests;
11 | using NServiceBus.AcceptanceTests.EndpointTemplates;
12 | using NUnit.Framework;
13 | using Routing;
14 | using Settings;
15 |
16 | public class When_using_a_custom_message_id_strategy : NServiceBusAcceptanceTest
17 | {
18 | const string CustomMessageId = "CustomMessageId";
19 |
20 | [Test]
21 | public async Task Should_use_custom_strategy_to_set_message_id_on_message_with_no_id()
22 | {
23 | var context = await Scenario.Define()
24 | .WithEndpoint()
25 | .Done(c => c.GotTheMessage)
26 | .Run();
27 |
28 | Assert.Multiple(() =>
29 | {
30 | Assert.That(context.GotTheMessage, Is.True, "Should receive the message");
31 | Assert.That(context.ReceivedMessageId, Is.EqualTo(CustomMessageId), "Message id should equal custom id value");
32 | });
33 | }
34 |
35 | public class Receiver : EndpointConfigurationBuilder
36 | {
37 | public Receiver()
38 | {
39 | EndpointSetup(c =>
40 | {
41 | c.EnableFeature();
42 | c.ConfigureRabbitMQTransport().MessageIdStrategy = m => CustomMessageId;
43 | });
44 | }
45 |
46 | class StarterFeature : Feature
47 | {
48 | protected override void Setup(FeatureConfigurationContext context)
49 | {
50 | context.Services.AddTransient();
51 | context.RegisterStartupTask(b => b.GetRequiredService());
52 | }
53 |
54 | class Starter : FeatureStartupTask
55 | {
56 | public Starter(IMessageDispatcher dispatchMessages, IReadOnlySettings settings)
57 | {
58 | this.dispatchMessages = dispatchMessages;
59 | this.settings = settings;
60 | }
61 |
62 | protected override Task OnStart(IMessageSession session, CancellationToken cancellationToken = default)
63 | {
64 | //Use feature to send message that has no message id
65 | var messageBody = "{}";
66 |
67 | var message = new OutgoingMessage(
68 | string.Empty,
69 | new Dictionary
70 | {
71 | { Headers.EnclosedMessageTypes, typeof(MyRequest).FullName }
72 | },
73 | Encoding.UTF8.GetBytes(messageBody));
74 |
75 | var transportOperation = new TransportOperation(message, new UnicastAddressTag(settings.EndpointName()));
76 | return dispatchMessages.Dispatch(new TransportOperations(transportOperation), new TransportTransaction(), cancellationToken);
77 | }
78 |
79 | protected override Task OnStop(IMessageSession session, CancellationToken cancellationToken = default) => Task.CompletedTask;
80 |
81 | readonly IMessageDispatcher dispatchMessages;
82 | readonly IReadOnlySettings settings;
83 | }
84 | }
85 |
86 | class MyEventHandler : IHandleMessages
87 | {
88 | readonly MyContext myContext;
89 |
90 | public MyEventHandler(MyContext myContext)
91 | {
92 | this.myContext = myContext;
93 | }
94 |
95 | public Task Handle(MyRequest message, IMessageHandlerContext context)
96 | {
97 | myContext.GotTheMessage = true;
98 | myContext.ReceivedMessageId = context.MessageId;
99 |
100 | return Task.CompletedTask;
101 | }
102 | }
103 | }
104 |
105 | public class MyRequest : IMessage
106 | {
107 | }
108 |
109 | class MyContext : ScenarioContext
110 | {
111 | public bool GotTheMessage { get; set; }
112 | public string ReceivedMessageId { get; set; }
113 | }
114 | }
115 | }
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/RabbitMQTransportInfrastructure.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ
2 | {
3 | using System;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading;
7 | using System.Threading.Tasks;
8 | using global::RabbitMQ.Client;
9 |
10 | sealed class RabbitMQTransportInfrastructure : TransportInfrastructure
11 | {
12 | readonly ConnectionFactory connectionFactory;
13 | readonly ChannelProvider channelProvider;
14 | readonly BrokerVerifier brokerVerifier;
15 | readonly IRoutingTopology routingTopology;
16 | readonly TimeSpan networkRecoveryInterval;
17 | readonly bool supportsDelayedDelivery;
18 |
19 | public RabbitMQTransportInfrastructure(HostSettings hostSettings, ReceiveSettings[] receiverSettings,
20 | ConnectionFactory connectionFactory, IRoutingTopology routingTopology,
21 | ChannelProvider channelProvider, MessageConverter messageConverter,
22 | BrokerVerifier brokerVerifier,
23 | Action messageCustomization,
24 | TimeSpan timeToWaitBeforeTriggeringCircuitBreaker, PrefetchCountCalculation prefetchCountCalculation,
25 | TimeSpan networkRecoveryInterval, bool supportsDelayedDelivery)
26 | {
27 | this.connectionFactory = connectionFactory;
28 | this.routingTopology = routingTopology;
29 | this.channelProvider = channelProvider;
30 | this.brokerVerifier = brokerVerifier;
31 | this.networkRecoveryInterval = networkRecoveryInterval;
32 | this.supportsDelayedDelivery = supportsDelayedDelivery;
33 |
34 | Dispatcher = new MessageDispatcher(channelProvider, messageCustomization, supportsDelayedDelivery);
35 | Receivers = receiverSettings.Select(x => CreateMessagePump(hostSettings, x, messageConverter, timeToWaitBeforeTriggeringCircuitBreaker, prefetchCountCalculation))
36 | .ToDictionary(x => x.Id, x => x);
37 | }
38 |
39 | IMessageReceiver CreateMessagePump(HostSettings hostSettings, ReceiveSettings settings, MessageConverter messageConverter, TimeSpan timeToWaitBeforeTriggeringCircuitBreaker, PrefetchCountCalculation prefetchCountCalculation)
40 | {
41 | var consumerTag = $"{hostSettings.HostDisplayName} - {hostSettings.Name}";
42 |
43 | return new MessagePump(settings, connectionFactory, routingTopology, messageConverter, consumerTag, channelProvider, brokerVerifier, timeToWaitBeforeTriggeringCircuitBreaker, prefetchCountCalculation, hostSettings.CriticalErrorAction, networkRecoveryInterval);
44 | }
45 |
46 | internal async Task SetupInfrastructure(string[] sendingQueues, CancellationToken cancellationToken = default)
47 | {
48 | using var connection = await connectionFactory.CreateAdministrationConnection(cancellationToken).ConfigureAwait(false);
49 |
50 | using var channel = await connection.CreateChannelAsync(cancellationToken: cancellationToken).ConfigureAwait(false);
51 |
52 | if (supportsDelayedDelivery)
53 | {
54 | await DelayInfrastructure.Build(channel, cancellationToken).ConfigureAwait(false);
55 | }
56 |
57 | var receivingQueues = Receivers.Select(r => r.Value.ReceiveAddress).ToArray();
58 |
59 | await routingTopology.Initialize(channel, receivingQueues, sendingQueues, cancellationToken).ConfigureAwait(false);
60 |
61 | if (supportsDelayedDelivery)
62 | {
63 | foreach (string receivingAddress in receivingQueues)
64 | {
65 | await routingTopology.BindToDelayInfrastructure(channel, receivingAddress,
66 | DelayInfrastructure.DeliveryExchange, DelayInfrastructure.BindingKey(receivingAddress), cancellationToken).ConfigureAwait(false);
67 | }
68 | }
69 | }
70 |
71 | public override async Task Shutdown(CancellationToken cancellationToken = default)
72 | {
73 | await Task.WhenAll(Receivers.Values.Select(r => r.StopReceive(cancellationToken)))
74 | .ConfigureAwait(false);
75 |
76 | await channelProvider.DisposeAsync().ConfigureAwait(false);
77 | brokerVerifier.Dispose();
78 | }
79 |
80 | public override string ToTransportAddress(QueueAddress address) => TranslateAddress(address);
81 |
82 | internal static string TranslateAddress(QueueAddress address)
83 | {
84 | var queue = new StringBuilder(address.BaseAddress);
85 | if (address.Discriminator != null)
86 | {
87 | queue.Append("-" + address.Discriminator);
88 | }
89 |
90 | if (address.Qualifier != null)
91 | {
92 | queue.Append("." + address.Qualifier);
93 | }
94 |
95 | return queue.ToString();
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/src/NServiceBus.Transport.RabbitMQ/Sending/BasicPropertiesExtensions.cs:
--------------------------------------------------------------------------------
1 | namespace NServiceBus.Transport.RabbitMQ
2 | {
3 | using System;
4 | using System.Globalization;
5 | using System.Linq;
6 | using global::RabbitMQ.Client;
7 |
8 | static class BasicPropertiesExtensions
9 | {
10 | public static void Fill(this IBasicProperties properties, OutgoingMessage message, DispatchProperties dispatchProperties)
11 | {
12 | if (message.MessageId != null)
13 | {
14 | properties.MessageId = message.MessageId;
15 | }
16 |
17 | var messageHeaders = message.Headers ?? [];
18 |
19 | var delayed = CalculateDelay(dispatchProperties, out var delay);
20 |
21 | properties.Persistent = !messageHeaders.Remove(UseNonPersistentDeliveryHeader);
22 |
23 | properties.Headers = messageHeaders.ToDictionary(p => p.Key, p => (object)p.Value);
24 |
25 | if (delayed)
26 | {
27 | properties.Headers[DelayInfrastructure.DelayHeader] = Convert.ToInt32(delay);
28 | }
29 |
30 | if (dispatchProperties.DiscardIfNotReceivedBefore != null && dispatchProperties.DiscardIfNotReceivedBefore.MaxTime < TimeSpan.MaxValue)
31 | {
32 | // align with TimeoutManager behavior
33 | if (delayed)
34 | {
35 | throw new Exception("Postponed delivery of messages with TimeToBeReceived set is not supported. Remove the TimeToBeReceived attribute to postpone messages of this type.");
36 | }
37 |
38 | properties.Expiration = dispatchProperties.DiscardIfNotReceivedBefore.MaxTime.TotalMilliseconds.ToString(CultureInfo.InvariantCulture);
39 | }
40 |
41 | if (messageHeaders.TryGetValue(NServiceBus.Headers.CorrelationId, out var correlationId) && correlationId != null)
42 | {
43 | properties.CorrelationId = correlationId;
44 | }
45 |
46 | if (messageHeaders.TryGetValue(NServiceBus.Headers.EnclosedMessageTypes, out var enclosedMessageTypes) && enclosedMessageTypes != null)
47 | {
48 | var index = enclosedMessageTypes.IndexOf(',');
49 |
50 | if (index > -1)
51 | {
52 | properties.Type = enclosedMessageTypes.Substring(0, index);
53 | }
54 | else
55 | {
56 | properties.Type = enclosedMessageTypes;
57 | }
58 | }
59 |
60 | if (messageHeaders.TryGetValue(NServiceBus.Headers.ContentType, out var contentType) && contentType != null)
61 | {
62 | properties.ContentType = contentType;
63 | }
64 | else
65 | {
66 | properties.ContentType = "application/octet-stream";
67 | }
68 |
69 | if (messageHeaders.TryGetValue(NServiceBus.Headers.ReplyToAddress, out var replyToAddress) && replyToAddress != null)
70 | {
71 | properties.ReplyTo = replyToAddress;
72 | }
73 | }
74 |
75 | static bool CalculateDelay(DispatchProperties dispatchProperties, out long delay)
76 | {
77 | delay = 0;
78 | var delayed = false;
79 |
80 | if (dispatchProperties.DoNotDeliverBefore != null)
81 | {
82 | delayed = true;
83 | delay = Convert.ToInt64(Math.Ceiling((dispatchProperties.DoNotDeliverBefore.At - DateTimeOffset.UtcNow).TotalSeconds));
84 |
85 | if (delay > DelayInfrastructure.MaxDelayInSeconds)
86 | {
87 | throw new Exception($"Message cannot set to be delivered at '{dispatchProperties.DoNotDeliverBefore.At}' because the delay specified via {nameof(DelayedDeliveryOptionExtensions.DoNotDeliverBefore)} exceeds the maximum delay value '{TimeSpan.FromSeconds(DelayInfrastructure.MaxDelayInSeconds)}'.");
88 | }
89 |
90 | }
91 | else if (dispatchProperties.DelayDeliveryWith != null)
92 | {
93 | delayed = true;
94 | delay = Convert.ToInt64(Math.Ceiling(dispatchProperties.DelayDeliveryWith.Delay.TotalSeconds));
95 |
96 | if (delay > DelayInfrastructure.MaxDelayInSeconds)
97 | {
98 | throw new Exception($"Message cannot be delayed by '{dispatchProperties.DelayDeliveryWith.Delay}' because the delay specified via {nameof(DelayedDeliveryOptionExtensions.DelayDeliveryWith)} exceeds the maximum delay value '{TimeSpan.FromSeconds(DelayInfrastructure.MaxDelayInSeconds)}'.");
99 | }
100 | }
101 |
102 | return delayed;
103 | }
104 |
105 | public const string UseNonPersistentDeliveryHeader = "NServiceBus.Transport.RabbitMQ.UseNonPersistentDelivery";
106 | }
107 | }
108 |
--------------------------------------------------------------------------------