├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ ├── feature_request.yml │ └── improvement_request.yml ├── SUPPORT.md ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── code-analysis.yml │ ├── nuget-audit.yml │ ├── release.yml │ ├── stale.yml │ └── virus-scan.yml ├── .gitignore ├── .mailmap ├── .repointegrity.yml ├── .reposync.yml ├── CONTRIBUTING.md ├── LICENSE.md ├── Package-README.md ├── README.md ├── SECURITY.md ├── global.json ├── nuget.config ├── reset-virtual-host.cmd ├── scripts └── Reset-RabbitMQ.ps1 └── src ├── .editorconfig ├── .globalconfig ├── Custom.Build.props ├── Directory.Build.props ├── Directory.Build.targets ├── NServiceBus.Transport.RabbitMQ.AcceptanceTests ├── .editorconfig ├── ChannelExtensions.cs ├── ConfigurationHelpers.cs ├── ConfigureEndpointRabbitMQTransport.cs ├── ConnectionHelper.cs ├── DelayedDelivery │ └── When_deferring_a_message_longer_than_allowed_maximum.cs ├── NServiceBus.Transport.RabbitMQ.AcceptanceTests.csproj ├── QuorumEndpoint.cs ├── QuorumQueues │ ├── When_classic_endpoint_uses_quorum_error_queue.cs │ ├── When_classic_endpoint_uses_quorum_queue.cs │ ├── When_immediate_retries_with_quorum_queues.cs │ ├── When_quorum_endpoint_uses_classic_error_queue.cs │ ├── When_quorum_endpoint_uses_classic_queue.cs │ └── When_starting_endpoint_using_quorum_queues.cs ├── TestSuiteConstraints.cs ├── When_customizing_outgoing_messages.cs ├── When_publishing_message_implementing_interface_in_direct_topology.cs ├── When_receiving_a_message.cs ├── When_receiving_a_reply_that_contains_a_legacy_callback_header.cs ├── When_requesting_non_persistent_delivery.cs ├── When_scaling_out_senders_that_uses_callbacks.cs ├── When_scaling_out_subscribers.cs ├── When_the_broker_connection_is_lost.cs ├── When_the_message_contains_a_legacy_callback_header.cs ├── When_using_a_custom_message_id_strategy.cs ├── When_using_a_custom_message_id_strategy_that_returns_an_invalid_message_id.cs └── When_using_direct_routing.cs ├── NServiceBus.Transport.RabbitMQ.CommandLine.Tests ├── .editorconfig ├── MigrateDelayedMessages │ └── When_migrating_a_delayed_message.cs ├── MigrateQueue │ └── QueueMigrateToQuorumTests.cs └── NServiceBus.Transport.RabbitMQ.CommandLine.Tests.csproj ├── NServiceBus.Transport.RabbitMQ.CommandLine ├── .editorconfig ├── Bind.cs ├── BrokerConnection.cs ├── BrokerConnectionBinder.cs ├── BrokerVerifierBinder.cs ├── Commands │ ├── Delays │ │ ├── DelaysCreateCommand.cs │ │ ├── DelaysMigrateCommand.cs │ │ └── DelaysVerifyCommand.cs │ ├── Endpoint │ │ └── EndpointCreateCommand.cs │ └── Queue │ │ ├── QueueMigrateCommand.cs │ │ └── QueueValidateDeliveryLimitCommand.cs ├── NServiceBus.Transport.RabbitMQ.CommandLine.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── RoutingTopologyBinder.cs ├── RoutingTopologyType.cs └── SharedOptions.cs ├── NServiceBus.Transport.RabbitMQ.Tests ├── .editorconfig ├── APIApprovals.cs ├── ApprovalFiles │ └── APIApprovals.Approve.approved.txt ├── BrokerVerifierTests.cs ├── Connection │ ├── ChannelProviderTests.cs │ ├── ConnectionConfigurationTests.cs │ └── ConnectionConfigurationWithAmqpTests.cs ├── DelayedDelivery │ └── When_calculating_a_routing_key.cs ├── ManagementClientTests.cs ├── MessageConverterTests.cs ├── NServiceBus.Transport.RabbitMQ.Tests.csproj ├── OutgoingMessageBuilder.cs ├── RabbitMqContext.cs ├── Support │ └── ConventionalRoutingTopologyExtensions.cs ├── When_consuming_from_classic_queues.cs ├── When_consuming_messages.cs ├── When_sending_a_message_over_rabbitmq.cs ├── When_stopping_endpoint.cs └── When_subscribed_to_a_event.cs ├── NServiceBus.Transport.RabbitMQ.TransportTests ├── .editorconfig ├── ConfigureRabbitMQTransportInfrastructure.cs ├── ConnectionHelper.cs ├── NServiceBus.Transport.RabbitMQ.TransportTests.csproj ├── When_changing_concurrency.cs ├── When_delayed_delivery_is_disabled.cs ├── When_modifying_incoming_headers.cs └── When_restarting_endpoint.cs ├── NServiceBus.Transport.RabbitMQ.slnx ├── NServiceBus.Transport.RabbitMQ ├── Administration │ ├── BrokerVerifier.cs │ ├── ManagementApi │ │ ├── Converters │ │ │ ├── DeliveryLimitConverter.cs │ │ │ ├── FeatureFlagStateConverter.cs │ │ │ ├── PolicyTargetConverter.cs │ │ │ └── QueueTypeConverter.cs │ │ ├── ManagementClient.cs │ │ └── Models │ │ │ ├── Binding.cs │ │ │ ├── FeatureFlag.cs │ │ │ ├── FeatureFlagExtensions.cs │ │ │ ├── GetQueuesResult.cs │ │ │ ├── Overview.cs │ │ │ ├── Policy.cs │ │ │ ├── PolicyDefinition.cs │ │ │ ├── PolicyTarget.cs │ │ │ ├── Queue.cs │ │ │ ├── QueueArguments.cs │ │ │ ├── QueueMessageStats.cs │ │ │ └── QueueType.cs │ ├── QueuePurger.cs │ └── SubscriptionManager.cs ├── Configuration │ ├── BrokerRequirementChecks.cs │ ├── CertificateLoader.cs │ ├── ConnectionConfiguration.cs │ ├── ManagementApiConfiguration.cs │ ├── PrefetchCountCalculation.cs │ ├── QueueType.cs │ ├── RabbitMQTransportSettingsExtensions.cs │ └── RoutingTopology.cs ├── Connection │ ├── ChannelProvider.cs │ ├── ConfirmsAwareChannel.cs │ └── ConnectionFactory.cs ├── DelayedDelivery │ └── DelayInfrastructure.cs ├── ExceptionExtensions.cs ├── FodyWeavers.xml ├── NServiceBus.Transport.RabbitMQ.csproj ├── PreObsoleteAttribute.cs ├── RabbitMQTransport.cs ├── RabbitMQTransportInfrastructure.cs ├── Receiving │ ├── MessageConverter.cs │ ├── MessagePump.cs │ ├── MessagePumpConnectionFailedCircuitBreaker.cs │ └── ModelExtensions.cs ├── Routing │ ├── ConventionalRoutingTopology.cs │ ├── DefaultRoutingKeyConvention.cs │ ├── DirectRoutingTopology.cs │ └── IRoutingTopology.cs └── Sending │ ├── BasicPropertiesExtensions.cs │ ├── MessageDispatcher.cs │ └── NonPersistentDeliveryModeExtensions.cs ├── NServiceBus.snk ├── NServiceBusTests.snk ├── msbuild ├── AutomaticVersionRanges.targets └── ConvertToVersionRange.cs └── targets ├── Broker.cs ├── Program.cs └── Targets.csproj /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | *.sh text eol=lf 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Looking for Support 2 | 3 | Check out our [support options](https://particular.net/support). -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | registries: 3 | particular-packages: 4 | type: nuget-feed 5 | url: https://f.feedz.io/particular-software/packages/nuget/index.json 6 | updates: 7 | - package-ecosystem: nuget 8 | directory: "/src" 9 | registries: "*" 10 | schedule: 11 | interval: daily 12 | open-pull-requests-limit: 1000 13 | groups: 14 | AWSSDK: 15 | patterns: 16 | - "AWSSDK.*" 17 | NServiceBusCore: 18 | patterns: 19 | - "NServiceBus" 20 | - "NServiceBus.AcceptanceTesting" 21 | - "NServiceBus.AcceptanceTests.Sources" 22 | - "NServiceBus.PersistenceTests.Sources" 23 | - "NServiceBus.TransportTests.Sources" 24 | ignore: 25 | # Particular.Analyzers updates are distributed via RepoStandards 26 | - dependency-name: "Particular.Analyzers" 27 | # Changing these 3 dependencies affects the .NET SDK and Visual Studio versions we support 28 | # These types of updates should be more intentional than an automated update 29 | - dependency-name: "Microsoft.Build.Utilities.Core" 30 | - dependency-name: "Microsoft.CodeAnalysis.CSharp" 31 | - dependency-name: "Microsoft.CodeAnalysis.CSharp.Workspaces" 32 | - package-ecosystem: "github-actions" 33 | directory: "/" 34 | schedule: 35 | interval: daily 36 | open-pull-requests-limit: 1000 37 | -------------------------------------------------------------------------------- /.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-2022 22 | name: Windows 23 | - os: ubuntu-22.04 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@v4.2.2 33 | with: 34 | fetch-depth: 0 35 | - name: Setup .NET SDK 36 | uses: actions/setup-dotnet@v4.3.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@v4.6.2 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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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-22.04 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4.2.2 18 | with: 19 | fetch-depth: 0 20 | - name: Setup .NET SDK 21 | uses: actions/setup-dotnet@v4.3.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@v4.6.2 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 | -------------------------------------------------------------------------------- /.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 }} -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | 71 | # Roslyn cache directories 72 | *.ide/ 73 | 74 | # MSTest test Results 75 | [Tt]est[Rr]esult*/ 76 | [Bb]uild[Ll]og.* 77 | 78 | #NUNIT 79 | *.VisualState.xml 80 | TestResult.xml 81 | 82 | # NCrunch 83 | _NCrunch_* 84 | .*crunch*.local.xml 85 | 86 | # ReSharper is a .NET coding add-in 87 | _ReSharper*/ 88 | *.[Rr]e[Ss]harper 89 | *.DotSettings 90 | *.DotSettings.user 91 | 92 | src/scaffolding.config 93 | 94 | # Approval tests temp file 95 | *.received.* 96 | 97 | # JetBrains Rider 98 | .idea/ 99 | *.sln.iml 100 | 101 | # Visual Studio Code 102 | .vscode 103 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /.reposync.yml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | For information on contributing, see https://docs.particular.net/platform/contributing. 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "10.0.0", 4 | "allowPrerelease": true, 5 | "rollForward": "latestFeature" 6 | } 7 | } -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /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/Custom.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11.0 5 | minor 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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/Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /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.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.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.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.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 | } -------------------------------------------------------------------------------- /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/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.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.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_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/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/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.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.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/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/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.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/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/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 | } -------------------------------------------------------------------------------- /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.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.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.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.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.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_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.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.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.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.CommandLine/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # Justification: Application 4 | dotnet_diagnostic.CA2007.severity = none 5 | -------------------------------------------------------------------------------- /src/NServiceBus.Transport.RabbitMQ.CommandLine/Bind.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.RabbitMQ.CommandLine 2 | { 3 | using System.CommandLine.Binding; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | static class Bind 7 | { 8 | public static ServiceProviderBinder FromServiceProvider() => ServiceProviderBinder.Instance; 9 | 10 | internal class ServiceProviderBinder : BinderBase 11 | { 12 | public static ServiceProviderBinder Instance { get; } = new(); 13 | 14 | protected override T GetBoundValue(BindingContext bindingContext) => (T)bindingContext.GetRequiredService(typeof(T)); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /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.CommandLine/BrokerConnectionBinder.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.RabbitMQ.CommandLine 2 | { 3 | using System.CommandLine; 4 | using System.CommandLine.Binding; 5 | using System.Security.Cryptography.X509Certificates; 6 | using NServiceBus.Transport.RabbitMQ.ManagementApi; 7 | 8 | class BrokerConnectionBinder(Option connectionStringOption, Option connectionStringEnvOption, Option managementApiUrlOption, Option managementApiUserNameOption, Option managementApiPasswordOption, Option certPathOption, 9 | Option certPassphraseOption, Option disableCertificateValidationOption, Option useExternalAuthOption) : BinderBase 10 | { 11 | protected override BrokerConnection GetBoundValue(BindingContext bindingContext) 12 | { 13 | var connectionStringOptionValue = bindingContext.ParseResult.GetValueForOption(connectionStringOption); 14 | var connectionStringEnvOptionValue = bindingContext.ParseResult.GetValueForOption(connectionStringEnvOption); 15 | var managementApiUrl = bindingContext.ParseResult.GetValueForOption(managementApiUrlOption); 16 | var managementApiUserName = bindingContext.ParseResult.GetValueForOption(managementApiUserNameOption); 17 | var managementApiPassword = bindingContext.ParseResult.GetValueForOption(managementApiPasswordOption); 18 | var certPath = bindingContext.ParseResult.GetValueForOption(certPathOption); 19 | var certPassphrase = bindingContext.ParseResult.GetValueForOption(certPassphraseOption); 20 | var disableCertificateValidation = bindingContext.ParseResult.GetValueForOption(disableCertificateValidationOption); 21 | var useExternalAuth = bindingContext.ParseResult.GetValueForOption(useExternalAuthOption); 22 | 23 | var connectionString = GetConnectionString(connectionStringOptionValue, connectionStringEnvOptionValue); 24 | 25 | var connectionConfiguration = ConnectionConfiguration.Create(connectionString); 26 | var managementApiConfiguration = ManagementApiConfiguration.Create(managementApiUrl, managementApiUserName, managementApiPassword); 27 | 28 | var managementClient = new ManagementClient(connectionConfiguration, managementApiConfiguration, disableCertificateValidation); 29 | var brokerVerifier = new BrokerVerifier(managementClient, BrokerRequirementChecks.None, true); 30 | 31 | X509Certificate2Collection? certificateCollection = null; 32 | 33 | if (certPath is not null) 34 | { 35 | certificateCollection = [CertificateLoader.LoadCertificateFromFile(certPath, certPassphrase)]; 36 | } 37 | 38 | var connectionFactory = new ConnectionFactory("rabbitmq-transport", connectionConfiguration, certificateCollection, disableCertificateValidation, useExternalAuth, TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(10), []); 39 | var brokerConnection = new BrokerConnection(brokerVerifier, connectionFactory); 40 | 41 | return brokerConnection; 42 | } 43 | 44 | static string GetConnectionString(string? connectionString, string? connectionStringEnv) 45 | { 46 | if (string.IsNullOrWhiteSpace(connectionString)) 47 | { 48 | var environment = Environment.GetEnvironmentVariable(connectionStringEnv ?? string.Empty); 49 | 50 | if (environment != null) 51 | { 52 | return environment; 53 | } 54 | } 55 | 56 | return connectionString ?? string.Empty; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/NServiceBus.Transport.RabbitMQ.CommandLine/BrokerVerifierBinder.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.RabbitMQ.CommandLine 2 | { 3 | using System.CommandLine; 4 | using System.CommandLine.Binding; 5 | using NServiceBus.Transport.RabbitMQ.ManagementApi; 6 | 7 | class BrokerVerifierBinder(Option connectionStringOption, Option connectionStringEnvOption, Option managementApiUrlOption, Option managementApiUserNameOption, Option managementApiPasswordOption, Option disableCertificateValidationOption) : BinderBase 8 | { 9 | protected override BrokerVerifier GetBoundValue(BindingContext bindingContext) 10 | { 11 | var connectionStringOptionValue = bindingContext.ParseResult.GetValueForOption(connectionStringOption); 12 | var connectionStringEnvOptionValue = bindingContext.ParseResult.GetValueForOption(connectionStringEnvOption); 13 | var managementApiUrl = bindingContext.ParseResult.GetValueForOption(managementApiUrlOption); 14 | var managementApiUserName = bindingContext.ParseResult.GetValueForOption(managementApiUserNameOption); 15 | var managementApiPassword = bindingContext.ParseResult.GetValueForOption(managementApiPasswordOption); 16 | var disableCertificateValidation = bindingContext.ParseResult.GetValueForOption(disableCertificateValidationOption); 17 | 18 | var connectionString = GetConnectionString(connectionStringOptionValue, connectionStringEnvOptionValue); 19 | 20 | var connectionConfiguration = ConnectionConfiguration.Create(connectionString); 21 | var managementApiConfiguration = ManagementApiConfiguration.Create(managementApiUrl, managementApiUserName, managementApiPassword); 22 | 23 | var managementClient = new ManagementClient(connectionConfiguration, managementApiConfiguration, disableCertificateValidation); 24 | var brokerVerifier = new BrokerVerifier(managementClient, BrokerRequirementChecks.None, true); 25 | 26 | return brokerVerifier; 27 | } 28 | 29 | static string GetConnectionString(string? connectionString, string? connectionStringEnv) 30 | { 31 | if (string.IsNullOrWhiteSpace(connectionString)) 32 | { 33 | var environment = Environment.GetEnvironmentVariable(connectionStringEnv ?? string.Empty); 34 | 35 | if (environment != null) 36 | { 37 | return environment; 38 | } 39 | } 40 | 41 | return connectionString ?? string.Empty; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/NServiceBus.Transport.RabbitMQ.CommandLine/Commands/Delays/DelaysCreateCommand.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.RabbitMQ.CommandLine 2 | { 3 | using System.CommandLine; 4 | 5 | class DelaysCreateCommand 6 | { 7 | public static Command CreateCommand() 8 | { 9 | var command = new Command("create", "Create v2 delay infrastructure queues and exchanges"); 10 | 11 | var brokerConnectionBinder = SharedOptions.CreateBrokerConnectionBinderWithOptions(command); 12 | 13 | command.SetHandler(async (brokerConnection, console, cancellationToken) => 14 | { 15 | var delaysCreate = new DelaysCreateCommand(brokerConnection, console); 16 | await delaysCreate.Run(cancellationToken); 17 | }, 18 | brokerConnectionBinder, Bind.FromServiceProvider(), Bind.FromServiceProvider()); 19 | 20 | return command; 21 | } 22 | 23 | public DelaysCreateCommand(BrokerConnection brokerConnection, IConsole console) 24 | { 25 | this.brokerConnection = brokerConnection; 26 | this.console = console; 27 | } 28 | 29 | public async Task Run(CancellationToken cancellationToken = default) 30 | { 31 | console.WriteLine($"Creating v2 delay infrastructure queues and exchanges..."); 32 | 33 | using var connection = await brokerConnection.Create(cancellationToken); 34 | using var channel = await connection.CreateChannelAsync(cancellationToken: cancellationToken); 35 | 36 | await DelayInfrastructure.Build(channel, cancellationToken); 37 | 38 | console.WriteLine("Queues and exchanges created successfully"); 39 | } 40 | 41 | readonly BrokerConnection brokerConnection; 42 | readonly IConsole console; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /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, IConsole console) 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.SetHandler(async (brokerVerifier, console, cancellationToken) => 15 | { 16 | var delaysVerify = new DelaysVerifyCommand(brokerVerifier, console); 17 | await delaysVerify.Run(cancellationToken); 18 | }, 19 | brokerVerifierBinder, Bind.FromServiceProvider(), Bind.FromServiceProvider()); 20 | 21 | return command; 22 | } 23 | 24 | public async Task Run(CancellationToken cancellationToken = default) 25 | { 26 | try 27 | { 28 | await brokerVerifier.Initialize(cancellationToken); 29 | await brokerVerifier.VerifyRequirements(cancellationToken); 30 | 31 | console.WriteLine("All checks OK"); 32 | } 33 | catch (Exception ex) when (!ex.IsCausedBy(cancellationToken)) 34 | { 35 | console.WriteLine($"Fail: {ex.Message}"); 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, IConsole console) 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() 12 | { 13 | Name = "queueName", 14 | Description = "The name of the queue to validate" 15 | }; 16 | 17 | var brokerVerifierBinder = SharedOptions.CreateBrokerVerifierBinderWithOptions(command); 18 | 19 | command.AddArgument(queueNameArgument); 20 | 21 | command.SetHandler(async (queueName, brokerVerifier, console, cancellationToken) => 22 | { 23 | var validateCommand = new QueueValidateDeliveryLimitCommand(queueName, brokerVerifier, console); 24 | await validateCommand.Run(cancellationToken); 25 | }, 26 | queueNameArgument, brokerVerifierBinder, Bind.FromServiceProvider(), Bind.FromServiceProvider()); 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 | console.WriteLine(ex.Message); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /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.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 | return await rootCommand.InvokeAsync(args); 11 | 12 | void CreateDelaysCommand(Command rootCommand) 13 | { 14 | var delaysSubCommand = new Command("delays", "Commands to manage the queues and exchanges for the NServiceBus delay infrastructure"); 15 | 16 | delaysSubCommand.AddCommand(DelaysCreateCommand.CreateCommand()); 17 | delaysSubCommand.AddCommand(DelaysMigrateCommand.CreateCommand()); 18 | delaysSubCommand.AddCommand(DelaysVerifyCommand.CreateCommand()); 19 | 20 | rootCommand.AddCommand(delaysSubCommand); 21 | } 22 | 23 | void CreateEndpointCommand(Command rootCommand) 24 | { 25 | var endpointSubCommand = new Command("endpoint", "Commands to manage the infrastructure for an NServiceBus endpoint"); 26 | 27 | endpointSubCommand.AddCommand(EndpointCreateCommand.CreateCommand()); 28 | 29 | rootCommand.AddCommand(endpointSubCommand); 30 | } 31 | 32 | void CreateQueueCommand(Command rootCommand) 33 | { 34 | var queueSubCommand = new Command("queue", "Commands to manage individual queues"); 35 | 36 | queueSubCommand.AddCommand(QueueMigrateCommand.CreateCommand()); 37 | queueSubCommand.AddCommand(QueueValidateDeliveryLimitCommand.CreateCommand()); 38 | 39 | rootCommand.AddCommand(queueSubCommand); 40 | } 41 | -------------------------------------------------------------------------------- /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.CommandLine/RoutingTopologyBinder.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.RabbitMQ.CommandLine 2 | { 3 | using System.CommandLine; 4 | using System.CommandLine.Binding; 5 | 6 | class RoutingTopologyBinder : BinderBase 7 | { 8 | public RoutingTopologyBinder(Option routingTopologyTypeOption, Option useDurableEntitiesOption, Option queueTypeOption) 9 | { 10 | this.routingTopologyTypeOption = routingTopologyTypeOption; 11 | this.useDurableEntitiesOption = useDurableEntitiesOption; 12 | this.queueTypeOption = queueTypeOption; 13 | } 14 | 15 | protected override IRoutingTopology GetBoundValue(BindingContext bindingContext) 16 | { 17 | var routingTopologyType = bindingContext.ParseResult.GetValueForOption(routingTopologyTypeOption); 18 | var useDurableEntities = bindingContext.ParseResult.GetValueForOption(useDurableEntitiesOption); 19 | var queueType = bindingContext.ParseResult.GetValueForOption(queueTypeOption); 20 | 21 | return routingTopologyType switch 22 | { 23 | RoutingTopologyType.Conventional => new ConventionalRoutingTopology(useDurableEntities, queueType), 24 | RoutingTopologyType.Direct => new DirectRoutingTopology(useDurableEntities, queueType), 25 | _ => throw new InvalidOperationException() 26 | }; 27 | } 28 | 29 | readonly Option routingTopologyTypeOption; 30 | readonly Option useDurableEntitiesOption; 31 | readonly Option queueTypeOption; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/NServiceBus.Transport.RabbitMQ.CommandLine/RoutingTopologyType.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.RabbitMQ.CommandLine 2 | { 3 | enum RoutingTopologyType 4 | { 5 | Conventional, 6 | Direct 7 | } 8 | } -------------------------------------------------------------------------------- /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.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.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.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.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.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.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 | 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.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.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/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.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 | -------------------------------------------------------------------------------- /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/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.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.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.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 | } -------------------------------------------------------------------------------- /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.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /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/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/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/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 | _ => throw new JsonException($"Unknown queue type: {value}") 21 | }; 22 | } 23 | 24 | public override void Write(Utf8JsonWriter writer, QueueType queueType, JsonSerializerOptions options) 25 | { 26 | var value = queueType switch 27 | { 28 | QueueType.Quorum => "quorum", 29 | QueueType.Classic => "classic", 30 | QueueType.Stream => "stream", 31 | _ => throw new ArgumentOutOfRangeException(nameof(queueType), $"QueueType value out of range: {queueType}") 32 | }; 33 | 34 | writer.WriteStringValue(value); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /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/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/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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 in 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 ?? -1; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /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/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/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 | Classic, 8 | Quorum, 9 | Stream 10 | } 11 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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/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/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/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/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 | } -------------------------------------------------------------------------------- /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/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/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/NServiceBus.Transport.RabbitMQ/FodyWeavers.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /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 | 23 | NServiceBus.RabbitMQ 24 | RabbitMQ support for NServiceBus 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/NServiceBus.Transport.RabbitMQ/PreObsoleteAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus 2 | { 3 | using System; 4 | 5 | /// 6 | /// Meant for staging future obsoletes. 7 | /// 8 | [AttributeUsage(AttributeTargets.All)] 9 | sealed class PreObsoleteAttribute : Attribute 10 | { 11 | public PreObsoleteAttribute(string contextUrl) 12 | { 13 | ContextUrl = contextUrl; 14 | } 15 | 16 | public string ContextUrl { get; } 17 | 18 | public string ReplacementTypeOrMember { get; set; } 19 | 20 | public string Message { get; set; } 21 | 22 | public string Note { get; set; } 23 | } 24 | } -------------------------------------------------------------------------------- /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/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/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/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/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/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.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Particular/NServiceBus.RabbitMQ/dba627a5a2c50519d7a2466efe3f76c8d5c8828d/src/NServiceBus.snk -------------------------------------------------------------------------------- /src/NServiceBusTests.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Particular/NServiceBus.RabbitMQ/dba627a5a2c50519d7a2466efe3f76c8d5c8828d/src/NServiceBusTests.snk -------------------------------------------------------------------------------- /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/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/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/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/targets/Targets.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | Exe 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | --------------------------------------------------------------------------------