├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── bug_report.yml │ ├── config.yml │ ├── feature_request.md │ ├── 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 ├── .reposync.yml ├── CONTRIBUTING.md ├── LICENSE.md ├── Package-README.md ├── README.md ├── SECURITY.md ├── global.json ├── nuget.config └── src ├── .editorconfig ├── .globalconfig ├── AcceptanceTests ├── .editorconfig ├── AcceptanceTestExtensions.cs ├── ConfigureEndpointAzureServiceBusTransport.cs ├── NServiceBus.Transport.AzureServiceBus.AcceptanceTests.csproj ├── Receiving │ ├── When_message_visibility_expired.cs │ ├── When_receiving_a_message.cs │ └── When_receiving_a_message_from_legacy_asb.cs ├── Sending │ ├── When_batching_multiple_outgoing_small_messages.cs │ ├── When_customizing_an_outgoing_native_message.cs │ ├── When_customizing_outgoing_native_messages.cs │ ├── When_sending_to_a_topic.cs │ └── When_using_dispatcher_directly_within_transactionscope_and_conflicting_isolation_level.cs ├── Subscribing │ └── When_multi_subscribing_to_a_polymorphic_event_using_topic_mappings.cs ├── TestIndependenceMutator.cs ├── TestIndependenceSkipBehavior.cs ├── TestSuiteConstraints.cs ├── When_loading_from_options.cs ├── When_operating_with_least_privilege.cs └── When_using_token_credential_with_fully_qualified_namespace.cs ├── CommandLine ├── .editorconfig ├── CommandRunner.cs ├── MigrationTopologyEndpoint.cs ├── NServiceBus.Transport.AzureServiceBus.CommandLine.csproj ├── Program.cs ├── Queue.cs ├── Rule.cs ├── Subscription.cs ├── Topic.cs └── TopicPerEventTopologyEndpoint.cs ├── CommandLineTests ├── .editorconfig ├── CommandLineTests.cs └── NServiceBus.Transport.AzureServiceBus.CommandLine.Tests.csproj ├── Custom.Build.props ├── Directory.Build.props ├── Directory.Build.targets ├── MigrationAcceptanceTests ├── .editorconfig ├── ConfigureEndpointAzureServiceBusTransport.cs ├── Migration │ └── When_migrating.cs ├── NServiceBus.Transport.AzureServiceBus.Migration.AcceptanceTests.csproj └── Receiving │ ├── When_publishing_from_different_topics.cs │ └── When_publishing_sendonly_and_subscribing_on_different_topics.cs ├── NServiceBus.Transport.AzureServiceBus.sln ├── NServiceBus.snk ├── NServiceBusTests.snk ├── Tests ├── .editorconfig ├── APIApprovals.cs ├── ApprovalFiles │ ├── APIApprovals.Approve.approved.txt │ ├── MigrationTopologyCreatorTests.Should_create_default_single_topic_topology.approved.txt │ ├── MigrationTopologyCreatorTests.Should_create_single_topic_topology.approved.txt │ ├── MigrationTopologyCreatorTests.Should_hierarchy.approved.txt │ ├── MigrationTopologySubscriptionManagerTests.Should_create_topology_for_events_to_migrate.approved.txt │ ├── MigrationTopologySubscriptionManagerTests.Should_create_topology_for_migrated_and_not_migrated_events.approved.txt │ ├── MigrationTopologyTests.Should_self_validate.approved.txt │ ├── MigrationTopologyTests.Should_self_validate_consistency.approved.txt │ ├── TopicPerEventSubscriptionManagerTests.Should_create_topology_for_mapped_events.approved.txt │ ├── TopicPerEventSubscriptionManagerTests.Should_create_topology_for_unmapped_events.approved.txt │ └── TopicPerEventTopologyTests.Should_self_validate.approved.txt ├── EventRouting │ ├── EventityValidatorTests.cs │ ├── MigrationTopologyCreatorTests.cs │ ├── MigrationTopologySubscriptionManagerTests.cs │ ├── MigrationTopologyTests.cs │ ├── SubscribeEventToTopicsMapConverterTests.cs │ ├── TopicPerEventSubscriptionManagerTests.cs │ └── TopicPerEventTopologyTests.cs ├── FakeProcessor.cs ├── FakeReceiver.cs ├── FakeSender.cs ├── FakeServiceBusClient.cs ├── NServiceBus.Transport.AzureServiceBus.Tests.csproj ├── Receiving │ ├── MessagePumpTests.cs │ └── RepeatedFailuresOverTimeCircuitBreakerTests.cs ├── RecordingServiceBusAdministrationClient.cs ├── RecordingServiceBusClient.cs ├── Sending │ ├── MessageDispatcherTests.cs │ ├── MessageRegistryTests.cs │ └── OutgoingMessageExtensionsFixture.cs ├── SkdJsonSerializerContext.cs └── Testing │ └── TestableCustomizeNativeMessageExtensionsTests.cs ├── Transport ├── Administration │ ├── NamespacePermissions.cs │ ├── QueueCreator.cs │ └── TopologyCreator.cs ├── AzureServiceBusTransport.cs ├── AzureServiceBusTransportInfrastructure.cs ├── AzureServiceBusTransportSettingsExtensions.cs ├── AzureServiceBusTransportTransaction.cs ├── AzureServiceBusTransportTransactionExtensions.cs ├── Configuration │ ├── DiagnosticDescriptors.cs │ └── TransportMessageHeaders.cs ├── EventRouting │ ├── AzureServiceBusQueuesAttribute.cs │ ├── AzureServiceBusRulesAttribute.cs │ ├── AzureServiceBusSubscriptionsAttribute.cs │ ├── AzureServiceBusTopicsAttribute.cs │ ├── EntityValidator.cs │ ├── MigrationTopology.cs │ ├── MigrationTopologyCreator.cs │ ├── MigrationTopologyOptions.cs │ ├── MigrationTopologyOptionsValidator.cs │ ├── MigrationTopologySubscriptionManager.cs │ ├── SubscribedEventToTopicsMapConverter.cs │ ├── SubscriptionManager.cs │ ├── SubscriptionManagerCreationOptions.cs │ ├── TopicPerEventTopology.cs │ ├── TopicPerEventTopologySubscriptionManager.cs │ ├── TopicTopology.cs │ ├── TopologyOptions.cs │ ├── TopologyOptionsDisableValidationValidator.cs │ ├── TopologyOptionsSerializationContext.cs │ ├── TopologyOptionsValidator.cs │ └── ValidMigrationTopologyAttribute.cs ├── ExceptionExtensions.cs ├── FodyWeavers.xml ├── NServiceBus.Transport.AzureServiceBus.csproj ├── OutgoingNativeMessageCustomizationAction.cs ├── PreObsoleteAttribute.cs ├── Receiving │ ├── MessageExtensions.cs │ ├── MessagePump.cs │ ├── ProcessMessageEventArgsExtensions.cs │ ├── QueueAddressQualifier.cs │ └── RepeatedFailuresOverTimeCircuitBreaker.cs ├── Sending │ ├── CustomizeNativeMessageExtensions.cs │ ├── MessageDispatcher.cs │ ├── MessageSenderRegistry.cs │ ├── NativeMessageCustomizationBehavior.cs │ ├── NativeMessageCustomizationFeature.cs │ ├── NativeMessageCustomizer.cs │ ├── OutgoingMessageExtensions.cs │ └── OutgoingTransportOperationExtensions.cs ├── Testing │ └── TestableCustomizeNativeMessageExtensions.cs ├── Utilities │ └── TransactionExtensions.cs └── obsoletes-v5.cs ├── TransportTests ├── .editorconfig ├── ConfigureAzureServiceBusTransportInfrastructure.cs ├── NServiceBus.Transport.AzureServiceBus.TransportTests.csproj ├── When_MessageLockLostException_is_thrown_from_on_error.cs ├── When_ServiceBusTimeoutException_is_thrown_from_on_error.cs └── When_using_dlq_qualifier.cs └── msbuild ├── AutomaticVersionRanges.targets └── ConvertToVersionRange.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | *.sh text eol=lf 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior. If you have a repro repository, provide a link. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Versions:** 17 | - NuGet package: [e.g. 1.0.0-alpha0073] 18 | - OS: [e.g. Windows 10 build 1803] 19 | - .NET Version [e.g. .NET Core 2.1.300] 20 | 21 | **Additional context** 22 | Add any other context about the problem here. 23 | - Code repro 24 | - Screenshot 25 | -------------------------------------------------------------------------------- /.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.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Check if a feature was not already requested (open and closed issues).** 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.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 | dotnet-version: | 39 | 9.0.x 40 | 8.0.x 41 | - name: Build 42 | run: dotnet build src --configuration Release 43 | - name: Upload packages 44 | if: runner.os == 'Windows' 45 | uses: actions/upload-artifact@v4.6.2 46 | with: 47 | name: NuGet packages 48 | path: nugets/ 49 | retention-days: 7 50 | - name: Azure login 51 | uses: azure/login@v2.3.0 52 | with: 53 | creds: ${{ secrets.AZURE_ACI_CREDENTIALS }} 54 | - name: Setup Azure Service Bus 55 | uses: Particular/setup-azureservicebus-action@v2.0.0 56 | with: 57 | connection-string-name: AzureServiceBus_ConnectionString 58 | azure-credentials: ${{ secrets.AZURE_ACI_CREDENTIALS }} 59 | tag: ASBTransport 60 | - name: Run tests 61 | uses: Particular/run-tests-action@v1.7.0 62 | -------------------------------------------------------------------------------- /.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 | dotnet-version: 9.0.x 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 | -------------------------------------------------------------------------------- /.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.Transport.AzureServiceBus 2 | 3 | NServiceBus.Transport.AzureServiceBus enables the use of the [Azure Service Bus Brokered Messaging](https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-messaging-overview) service as the underlying transport used by NServiceBus. This transport uses the [Azure.Messaging.ServiceBus NuGet package](https://www.nuget.org/packages/Azure.Messaging.ServiceBus/). 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 [Azure Service Bus Transport documentation](https://docs.particular.net/transports/azure-service-bus/) for more details on how to use it. 8 | 9 | ## Running tests locally 10 | 11 | ### Acceptance Tests 12 | 13 | Follow these steps to run the acceptance tests locally: 14 | 15 | * Add a new environment variable `AzureServiceBus_ConnectionString` containing a connection string to your Azure Service Bus namespace. 16 | * Add a new environment variable `AzureServiceBus_ConnectionString_Restricted` containing a connection string to the same namespace with [`Send` and `Listen` rights](https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-sas#shared-access-authorization-policies) only. 17 | * Some tests are using `Azure.Identity` with the `DefaultAzureCredential` and require one of the supported credentials to be present locally. For more information see the [troubleshooting guideline](https://aka.ms/azsdk/net/identity/defaultazurecredential/troubleshoot) 18 | 19 | ### Unit Tests 20 | 21 | * Add a new environment variable `AzureServiceBus_ConnectionString` containing a connection string to your Azure Service Bus namespace (can be same as for acceptance tests). 22 | -------------------------------------------------------------------------------- /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": "9.0.100", 4 | "rollForward": "latestFeature" 5 | } 6 | } -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /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/AcceptanceTests/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # Justification: Test project 4 | dotnet_diagnostic.CA2007.severity = none 5 | 6 | # Justification: Cancellation may not be needed in test project 7 | dotnet_diagnostic.PS0018.severity = suggestion 8 | dotnet_diagnostic.PS0013.severity = suggestion 9 | 10 | # Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken 11 | dotnet_diagnostic.NSB0002.severity = suggestion 12 | -------------------------------------------------------------------------------- /src/AcceptanceTests/AcceptanceTestExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.AcceptanceTests; 2 | 3 | using System; 4 | using System.IO.Hashing; 5 | using System.Text; 6 | 7 | public static class AcceptanceTestExtensions 8 | { 9 | public static string ToTopicName(this Type eventType) => 10 | eventType.FullName.Replace("+", ".").Shorten(maxLength: 260); 11 | 12 | // The idea here is to preserve part of the text and append a non-cryptographic hash to it. 13 | // This way, we can have a deterministic and unique names without harming much the readability. 14 | // The chance of collisions should be very low but definitely not zero. We can always switch to 15 | // using more bits in the hash or even back to a cryptographic hash if needed. 16 | public static string Shorten(this string name, int maxLength = 50) 17 | { 18 | if (name.Length <= maxLength) 19 | { 20 | return name; 21 | } 22 | 23 | var nameBytes = Encoding.UTF8.GetBytes(name); 24 | var hashValue = XxHash32.Hash(nameBytes); 25 | string hashHex = Convert.ToHexString(hashValue); 26 | 27 | int prefixLength = maxLength - hashHex.Length; 28 | 29 | if (prefixLength < 0) 30 | { 31 | return hashHex.Length > maxLength 32 | ? hashHex[..maxLength] // in case even the hash is too long 33 | : hashHex; 34 | } 35 | 36 | string prefix = name[..Math.Min(prefixLength, name.Length)]; 37 | return $"{prefix}{hashHex}"; 38 | } 39 | } -------------------------------------------------------------------------------- /src/AcceptanceTests/ConfigureEndpointAzureServiceBusTransport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using NServiceBus; 6 | using NServiceBus.AcceptanceTesting.Customization; 7 | using NServiceBus.AcceptanceTesting.Support; 8 | using NServiceBus.AcceptanceTests.Routing; 9 | using NServiceBus.AcceptanceTests.Routing.NativePublishSubscribe; 10 | using NServiceBus.AcceptanceTests.Sagas; 11 | using NServiceBus.AcceptanceTests.Versioning; 12 | using NServiceBus.MessageMutator; 13 | using NServiceBus.Transport.AzureServiceBus; 14 | using NServiceBus.Transport.AzureServiceBus.AcceptanceTests; 15 | using Conventions = NServiceBus.AcceptanceTesting.Customization.Conventions; 16 | 17 | public class ConfigureEndpointAzureServiceBusTransport : IConfigureEndpointTestExecution 18 | { 19 | public Task Configure(string endpointName, EndpointConfiguration configuration, RunSettings settings, PublisherMetadata publisherMetadata) 20 | { 21 | var connectionString = Environment.GetEnvironmentVariable("AzureServiceBus_ConnectionString"); 22 | 23 | if (string.IsNullOrEmpty(connectionString)) 24 | { 25 | throw new InvalidOperationException("envvar AzureServiceBus_ConnectionString not set"); 26 | } 27 | 28 | var topology = TopicTopology.Default; 29 | topology.OverrideSubscriptionNameFor(endpointName, endpointName.Shorten()); 30 | 31 | foreach (var eventType in publisherMetadata.Publishers.SelectMany(p => p.Events)) 32 | { 33 | topology.PublishTo(eventType, eventType.ToTopicName()); 34 | topology.SubscribeTo(eventType, eventType.ToTopicName()); 35 | } 36 | 37 | var transport = new AzureServiceBusTransport(connectionString, topology); 38 | 39 | ApplyMappingsToSupportMultipleInheritance(endpointName, topology); 40 | 41 | configuration.UseTransport(transport); 42 | 43 | configuration.RegisterComponents(c => c.AddSingleton()); 44 | configuration.Pipeline.Register("TestIndependenceBehavior", typeof(TestIndependenceSkipBehavior), "Skips messages not created during the current test."); 45 | 46 | configuration.EnforcePublisherMetadataRegistration(endpointName, publisherMetadata); 47 | 48 | return Task.CompletedTask; 49 | } 50 | 51 | static void ApplyMappingsToSupportMultipleInheritance(string endpointName, TopicPerEventTopology topology) 52 | { 53 | if (endpointName == Conventions.EndpointNamingConvention(typeof(MultiSubscribeToPolymorphicEvent.Subscriber))) 54 | { 55 | topology.SubscribeTo(typeof(MultiSubscribeToPolymorphicEvent.MyEvent1).ToTopicName()); 56 | topology.SubscribeTo(typeof(MultiSubscribeToPolymorphicEvent.MyEvent2).ToTopicName()); 57 | } 58 | 59 | if (endpointName == Conventions.EndpointNamingConvention(typeof(When_subscribing_to_a_base_event.GeneralSubscriber))) 60 | { 61 | topology.SubscribeTo(typeof(When_subscribing_to_a_base_event.SpecificEvent).ToTopicName()); 62 | } 63 | 64 | if (endpointName == Conventions.EndpointNamingConvention( 65 | typeof(When_publishing_an_event_implementing_two_unrelated_interfaces.Subscriber))) 66 | { 67 | topology.SubscribeTo( 68 | typeof(When_publishing_an_event_implementing_two_unrelated_interfaces.CompositeEvent).ToTopicName()); 69 | topology.SubscribeTo( 70 | typeof(When_publishing_an_event_implementing_two_unrelated_interfaces.CompositeEvent).ToTopicName()); 71 | } 72 | 73 | if (endpointName == Conventions.EndpointNamingConvention( 74 | typeof(When_started_by_base_event_from_other_saga.SagaThatIsStartedByABaseEvent))) 75 | { 76 | topology.SubscribeTo( 77 | typeof(When_started_by_base_event_from_other_saga.ISomethingHappenedEvent).ToTopicName()); 78 | } 79 | 80 | if (endpointName == Conventions.EndpointNamingConvention( 81 | typeof(When_multiple_versions_of_a_message_is_published.V1Subscriber))) 82 | { 83 | topology.SubscribeTo( 84 | typeof(When_multiple_versions_of_a_message_is_published.V2Event).ToTopicName()); 85 | } 86 | } 87 | 88 | public Task Cleanup() => Task.CompletedTask; 89 | } 90 | -------------------------------------------------------------------------------- /src/AcceptanceTests/NServiceBus.Transport.AzureServiceBus.AcceptanceTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.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/AcceptanceTests/Receiving/When_message_visibility_expired.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.AcceptanceTests 2 | { 3 | using System; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using AcceptanceTesting; 7 | using Azure.Messaging.ServiceBus; 8 | using NServiceBus.AcceptanceTests; 9 | using NServiceBus.AcceptanceTests.EndpointTemplates; 10 | using NUnit.Framework; 11 | 12 | public class When_message_visibility_expired : NServiceBusAcceptanceTest 13 | { 14 | [Test] 15 | public async Task Should_complete_message_on_next_receive_when_pipeline_successful() 16 | { 17 | var ctx = await Scenario.Define() 18 | .WithEndpoint(b => 19 | { 20 | b.CustomConfig(c => 21 | { 22 | // Limiting the concurrency for this test to make sure messages that are made available again are 23 | // not concurrently processed. This is not necessary for the test to pass but it makes 24 | // reasoning about the test easier. 25 | c.LimitMessageProcessingConcurrencyTo(1); 26 | }); 27 | b.When((session, _) => session.SendLocal(new MyMessage())); 28 | }) 29 | .Done(c => c.NativeMessageId is not null && c.Logs.Any(l => WasMarkedAsSuccessfullyCompleted(l, c))) 30 | .Run(); 31 | 32 | var items = ctx.Logs.Where(l => WasMarkedAsSuccessfullyCompleted(l, ctx)).ToArray(); 33 | 34 | Assert.That(items, Is.Not.Empty); 35 | } 36 | 37 | [Test] 38 | public async Task Should_complete_message_on_next_receive_when_error_pipeline_handled_the_message() 39 | { 40 | var ctx = await Scenario.Define(c => 41 | { 42 | c.ShouldThrow = true; 43 | }) 44 | .WithEndpoint(b => 45 | { 46 | b.DoNotFailOnErrorMessages(); 47 | b.CustomConfig(c => 48 | { 49 | var recoverability = c.Recoverability(); 50 | recoverability.AddUnrecoverableException(); 51 | 52 | // Limiting the concurrency for this test to make sure messages that are made available again are 53 | // not concurrently processed. This is not necessary for the test to pass but it makes 54 | // reasoning about the test easier. 55 | c.LimitMessageProcessingConcurrencyTo(1); 56 | }); 57 | b.When((session, _) => session.SendLocal(new MyMessage())); 58 | }) 59 | .Done(c => c.NativeMessageId is not null && c.Logs.Any(l => WasMarkedAsSuccessfullyCompleted(l, c))) 60 | .Run(); 61 | 62 | var items = ctx.Logs.Where(l => WasMarkedAsSuccessfullyCompleted(l, ctx)).ToArray(); 63 | 64 | Assert.That(items, Is.Not.Empty); 65 | } 66 | 67 | static bool WasMarkedAsSuccessfullyCompleted(ScenarioContext.LogItem l, Context c) 68 | => l.Message.StartsWith($"Received message with id '{c.NativeMessageId}' was marked as successfully completed"); 69 | 70 | class Context : ScenarioContext 71 | { 72 | public bool ShouldThrow { get; set; } 73 | 74 | public string NativeMessageId { get; set; } 75 | } 76 | 77 | class Receiver : EndpointConfigurationBuilder 78 | { 79 | public Receiver() => EndpointSetup(c => 80 | { 81 | var transport = c.ConfigureTransport(); 82 | // Explicitly setting the transport transaction mode to ReceiveOnly because the message 83 | // tracking only is implemented for this mode. 84 | transport.TransportTransactionMode = TransportTransactionMode.ReceiveOnly; 85 | }); 86 | } 87 | 88 | public class MyMessage : IMessage; 89 | 90 | class MyMessageHandler(Context testContext) : IHandleMessages 91 | { 92 | public async Task Handle(MyMessage message, IMessageHandlerContext context) 93 | { 94 | var messageEventArgs = context.Extensions.Get(); 95 | // By abandoning the message, the message will be "immediately available" for retrieval again and effectively the message pump 96 | // has lost the message visibility timeout because any Complete or Abandon will be rejected by the azure service bus. 97 | var serviceBusReceivedMessage = context.Extensions.Get(); 98 | await messageEventArgs.AbandonMessageAsync(serviceBusReceivedMessage); 99 | 100 | testContext.NativeMessageId = serviceBusReceivedMessage.MessageId; 101 | 102 | if (testContext.ShouldThrow) 103 | { 104 | throw new InvalidOperationException("Simulated exception"); 105 | } 106 | } 107 | } 108 | } 109 | } -------------------------------------------------------------------------------- /src/AcceptanceTests/Receiving/When_receiving_a_message.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.AcceptanceTests.Receiving 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using AcceptanceTesting; 6 | using Azure.Messaging.ServiceBus; 7 | using NServiceBus.AcceptanceTests.EndpointTemplates; 8 | using NUnit.Framework; 9 | using Pipeline; 10 | 11 | public class When_receiving_a_message 12 | { 13 | [Test] 14 | public async Task Should_have_access_to_the_native_message_via_extensions() 15 | { 16 | await Scenario.Define() 17 | .WithEndpoint(b => b.When( 18 | (session, c) => session.SendLocal(new Message()))) 19 | .Done(c => c.NativeMessageFound) 20 | .Run(); 21 | } 22 | 23 | public class Context : ScenarioContext 24 | { 25 | public bool NativeMessageFound { get; set; } 26 | } 27 | 28 | public class Endpoint : EndpointConfigurationBuilder 29 | { 30 | public Endpoint() 31 | { 32 | EndpointSetup((c, d) => 33 | c.Pipeline.Register(b => new CheckContextForValidUntilUtc((Context)d.ScenarioContext), "Behavior to validate context bag contains the original brokered message")); 34 | } 35 | 36 | public class Handler : IHandleMessages 37 | { 38 | Context testContext; 39 | 40 | public Handler(Context testContext) 41 | { 42 | this.testContext = testContext; 43 | } 44 | 45 | public Task Handle(Message request, IMessageHandlerContext context) 46 | { 47 | testContext.NativeMessageFound = testContext.NativeMessageFound && context.Extensions.Get() != null; 48 | 49 | return Task.CompletedTask; 50 | } 51 | } 52 | 53 | public class CheckContextForValidUntilUtc : Behavior 54 | { 55 | readonly Context testContext; 56 | 57 | public CheckContextForValidUntilUtc(Context context) 58 | { 59 | testContext = context; 60 | } 61 | 62 | public override Task Invoke(ITransportReceiveContext context, Func next) 63 | { 64 | testContext.NativeMessageFound = context.Extensions.Get() != null; 65 | 66 | return next(); 67 | } 68 | } 69 | } 70 | 71 | public class Message : IMessage { } 72 | } 73 | } -------------------------------------------------------------------------------- /src/AcceptanceTests/Receiving/When_receiving_a_message_from_legacy_asb.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.AcceptanceTests.Receiving 2 | { 3 | using System; 4 | using System.IO; 5 | using System.Runtime.Serialization; 6 | using System.Threading.Tasks; 7 | using System.Xml; 8 | using NServiceBus.AcceptanceTesting; 9 | using NServiceBus.AcceptanceTests.EndpointTemplates; 10 | using NUnit.Framework; 11 | 12 | class When_receiving_a_message_from_legacy_asb 13 | { 14 | [Test] 15 | public async Task Should_process_message_correctly() 16 | { 17 | await Scenario.Define() 18 | .WithEndpoint(b => b.When((session, c) => 19 | { 20 | var sendOptions = new SendOptions(); 21 | sendOptions.RouteToThisEndpoint(); 22 | sendOptions.CustomizeNativeMessage(msg => 23 | { 24 | msg.ApplicationProperties["NServiceBus.Transport.Encoding"] = "wcf/byte-array"; 25 | 26 | var serializer = new DataContractSerializer(typeof(byte[])); 27 | using var stream = new MemoryStream(); 28 | using var writer = XmlDictionaryWriter.CreateBinaryWriter(stream); 29 | serializer.WriteObject(writer, msg.Body.ToArray()); 30 | writer.Flush(); 31 | 32 | msg.Body = new BinaryData(stream.ToArray()); 33 | }); 34 | return session.Send(new Message(), sendOptions); 35 | })) 36 | .Done(c => c.MessageRecieved) 37 | .Run(); 38 | } 39 | 40 | public class Context : ScenarioContext 41 | { 42 | public bool MessageRecieved { get; set; } 43 | } 44 | 45 | public class Endpoint : EndpointConfigurationBuilder 46 | { 47 | public Endpoint() => EndpointSetup(); 48 | 49 | public class Handler(Context testContext) : IHandleMessages 50 | { 51 | public Task Handle(Message request, IMessageHandlerContext context) 52 | { 53 | testContext.MessageRecieved = true; 54 | 55 | return Task.CompletedTask; 56 | } 57 | } 58 | } 59 | 60 | public class Message : IMessage; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/AcceptanceTests/Sending/When_customizing_outgoing_native_messages.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.RabbitMQ.AcceptanceTests.Sending 2 | { 3 | using System.Threading.Tasks; 4 | using AcceptanceTesting; 5 | using Azure.Messaging.ServiceBus; 6 | using NServiceBus.AcceptanceTests; 7 | using NServiceBus.AcceptanceTests.EndpointTemplates; 8 | using NUnit.Framework; 9 | 10 | class When_customizing_outgoing_native_messages : NServiceBusAcceptanceTest 11 | { 12 | const string TestSubject = "0192c3ad-8ab2-77a0-8a92-2be53f062e06"; 13 | 14 | [Test] 15 | public async Task Should_dispatch_native_message_with_the_customizations() 16 | { 17 | var scenario = await Scenario.Define() 18 | .WithEndpoint(b => b.When((bus, c) => bus.SendLocal(new Message()))) 19 | .Done(c => c.ReceivedMessage != null) 20 | .Run(); 21 | 22 | Assert.That(scenario.ReceivedMessage.Subject, Is.EqualTo(TestSubject)); 23 | } 24 | 25 | class Context : ScenarioContext 26 | { 27 | public ServiceBusReceivedMessage ReceivedMessage { get; set; } 28 | } 29 | 30 | public class Receiver : EndpointConfigurationBuilder 31 | { 32 | public Receiver() => 33 | EndpointSetup(endpointConfiguration => 34 | { 35 | var transport = endpointConfiguration.ConfigureTransport(); 36 | transport.OutgoingNativeMessageCustomization = (_, message) => message.Subject = TestSubject; 37 | }); 38 | 39 | class MyEventHandler(Context testContext) : IHandleMessages 40 | { 41 | public Task Handle(Message message, IMessageHandlerContext context) 42 | { 43 | testContext.ReceivedMessage = context.Extensions.Get(); 44 | return Task.CompletedTask; 45 | } 46 | } 47 | } 48 | 49 | public class Message : IMessage; 50 | } 51 | } -------------------------------------------------------------------------------- /src/AcceptanceTests/Sending/When_sending_to_a_topic.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.AcceptanceTests.Sending 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using AcceptanceTesting; 6 | using AcceptanceTesting.Customization; 7 | using Azure.Messaging.ServiceBus.Administration; 8 | using NServiceBus.AcceptanceTests; 9 | using NServiceBus.AcceptanceTests.EndpointTemplates; 10 | using NServiceBus.Configuration.AdvancedExtensibility; 11 | using NUnit.Framework; 12 | 13 | // Azure Service Bus SDK doesn't differentiate between sending to a topic or a queue 14 | // There are valid scenarios where a message is sent to a topic and then forwarded to a specific queue 15 | // using rules. This is for example common when you to do some form of replication like describe in 16 | // https://github.com/Azure-Samples/azure-messaging-replication-dotnet/tree/main/functions/code/ServiceBusActivePassive or 17 | // in cases where you want to route the command to a specific endpoint based on some property of the message 18 | // This test makes sure that this scenario is supported and broken over time. 19 | public class When_sending_to_a_topic : NServiceBusAcceptanceTest 20 | { 21 | static string TopicName; 22 | 23 | [SetUp] 24 | public async Task Setup() 25 | { 26 | TopicName = "SendingToATopic"; 27 | 28 | var adminClient = 29 | new ServiceBusAdministrationClient( 30 | Environment.GetEnvironmentVariable("AzureServiceBus_ConnectionString")); 31 | 32 | if (await adminClient.TopicExistsAsync(TopicName)) 33 | { 34 | // makes sure during local development the topic gets cleared before each test run 35 | await adminClient.DeleteTopicAsync(TopicName); 36 | } 37 | 38 | await adminClient.CreateTopicAsync(TopicName); 39 | string endpointName = Conventions.EndpointNamingConvention(typeof(Receiver)).Shorten(); 40 | if (!await adminClient.QueueExistsAsync(endpointName)) 41 | { 42 | await adminClient.CreateQueueAsync(endpointName); 43 | } 44 | await adminClient.CreateSubscriptionAsync(new CreateSubscriptionOptions(TopicName, endpointName) 45 | { 46 | ForwardTo = endpointName, 47 | }); 48 | } 49 | 50 | [Test] 51 | public async Task Should_receive_the_message_assuming_correct_forwarding_rules() 52 | { 53 | var context = await Scenario.Define() 54 | .WithEndpoint(b => b.When(session => session.Send(new MyMessage()))) 55 | .WithEndpoint() 56 | .Done(c => c.MessageReceived) 57 | .Run(); 58 | 59 | Assert.That(context.MessageReceived, Is.True); 60 | } 61 | 62 | public class Context : ScenarioContext 63 | { 64 | public bool MessageReceived { get; set; } 65 | } 66 | 67 | public class Sender : EndpointConfigurationBuilder 68 | { 69 | public Sender() => 70 | EndpointSetup(builder => 71 | { 72 | builder.ConfigureRouting().RouteToEndpoint(typeof(MyMessage), TopicName); 73 | }); 74 | } 75 | 76 | public class Receiver : EndpointConfigurationBuilder 77 | { 78 | public Receiver() => EndpointSetup(c => 79 | { 80 | c.GetSettings().Set("Installers.Enable", false); 81 | }); 82 | 83 | public class MyMessageHandler(Context testContext) : IHandleMessages 84 | { 85 | public Task Handle(MyMessage message, IMessageHandlerContext context) 86 | { 87 | testContext.MessageReceived = true; 88 | return Task.FromResult(0); 89 | } 90 | } 91 | } 92 | 93 | public class MyMessage : ICommand; 94 | } 95 | } -------------------------------------------------------------------------------- /src/AcceptanceTests/Sending/When_using_dispatcher_directly_within_transactionscope_and_conflicting_isolation_level.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using System.Transactions; 4 | using NServiceBus; 5 | using NServiceBus.AcceptanceTesting; 6 | using NServiceBus.AcceptanceTests; 7 | using NServiceBus.AcceptanceTests.EndpointTemplates; 8 | using NServiceBus.Features; 9 | using NUnit.Framework; 10 | 11 | class When_sending_message_outside_of_a_handler_with_incorrect_transaction_scope : NServiceBusAcceptanceTest 12 | { 13 | [Test] 14 | public async Task Should_dispatch_message() 15 | { 16 | await Scenario.Define() 17 | .WithEndpoint() 18 | .Done(context => context.Received) 19 | .Run(); 20 | } 21 | 22 | class Receiver : EndpointConfigurationBuilder 23 | { 24 | public Receiver() 25 | { 26 | EndpointSetup(c => c.EnableFeature()); 27 | } 28 | 29 | public class MyMessageHandler : IHandleMessages 30 | { 31 | Context testContext; 32 | 33 | public MyMessageHandler(Context testContext) 34 | { 35 | this.testContext = testContext; 36 | } 37 | 38 | public Task Handle(MyMessage message, IMessageHandlerContext context) 39 | { 40 | testContext.Received = true; 41 | 42 | return Task.CompletedTask; 43 | } 44 | } 45 | 46 | class SendMessageFeature : Feature 47 | { 48 | protected override void Setup(FeatureConfigurationContext context) 49 | { 50 | context.RegisterStartupTask(builder => new StartupTask()); 51 | } 52 | } 53 | 54 | class StartupTask : FeatureStartupTask 55 | { 56 | protected override async Task OnStart(IMessageSession session, CancellationToken cancellationToken = default) 57 | { 58 | using (var tx = new TransactionScope(TransactionScopeOption.RequiresNew, new TransactionOptions { IsolationLevel = IsolationLevel.ReadCommitted }, TransactionScopeAsyncFlowOption.Enabled)) 59 | { 60 | await session.SendLocal(new MyMessage(), cancellationToken); 61 | 62 | tx.Complete(); 63 | } 64 | } 65 | 66 | protected override Task OnStop(IMessageSession session, CancellationToken cancellationToken = default) 67 | { 68 | return Task.CompletedTask; 69 | } 70 | } 71 | } 72 | 73 | class Context : ScenarioContext 74 | { 75 | public bool Received { get; set; } 76 | } 77 | 78 | class MyMessage : IMessage { } 79 | } -------------------------------------------------------------------------------- /src/AcceptanceTests/Subscribing/When_multi_subscribing_to_a_polymorphic_event_using_topic_mappings.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.AcceptanceTests.NativePubSub 2 | { 3 | using System.Threading.Tasks; 4 | using AcceptanceTesting; 5 | using AcceptanceTesting.Customization; 6 | using EndpointTemplates; 7 | using NUnit.Framework; 8 | using Transport.AzureServiceBus.AcceptanceTests; 9 | 10 | public class When_multi_subscribing_to_a_polymorphic_event_using_topic_mappings : NServiceBusAcceptanceTest 11 | { 12 | [Test] 13 | public async Task Both_events_should_be_delivered() 14 | { 15 | Requires.NativePubSubSupport(); 16 | 17 | var context = await Scenario.Define() 18 | .WithEndpoint(b => b.When((session, c) => 19 | { 20 | c.AddTrace("Publishing MyEvent1"); 21 | return session.Publish(new MyEvent1()); 22 | })) 23 | .WithEndpoint(b => b.When((session, c) => 24 | { 25 | c.AddTrace("Publishing MyEvent2"); 26 | return session.Publish(new MyEvent2()); 27 | })) 28 | .WithEndpoint() 29 | .Done(c => c.SubscriberGotMyEvent1 && c.SubscriberGotMyEvent2) 30 | .Run(); 31 | 32 | Assert.Multiple(() => 33 | { 34 | Assert.That(context.SubscriberGotMyEvent1, Is.True); 35 | Assert.That(context.SubscriberGotMyEvent2, Is.True); 36 | }); 37 | } 38 | 39 | public class Context : ScenarioContext 40 | { 41 | public bool SubscriberGotMyEvent1 { get; set; } 42 | public bool SubscriberGotMyEvent2 { get; set; } 43 | } 44 | 45 | public class Publisher1 : EndpointConfigurationBuilder 46 | { 47 | public Publisher1() => 48 | EndpointSetup(c => 49 | { 50 | var transport = c.ConfigureTransport(); 51 | 52 | var topology = TopicTopology.Default; 53 | topology.PublishTo(typeof(MyEvent1).ToTopicName()); 54 | transport.Topology = topology; 55 | }, metadata => metadata.RegisterSelfAsPublisherFor(this)); 56 | } 57 | 58 | public class Publisher2 : EndpointConfigurationBuilder 59 | { 60 | public Publisher2() => 61 | EndpointSetup(c => 62 | { 63 | var transport = c.ConfigureTransport(); 64 | 65 | var topology = TopicTopology.Default; 66 | topology.PublishTo(typeof(MyEvent2).ToTopicName()); 67 | transport.Topology = topology; 68 | }, metadata => metadata.RegisterSelfAsPublisherFor(this)); 69 | } 70 | 71 | public class Subscriber : EndpointConfigurationBuilder 72 | { 73 | public Subscriber() => 74 | EndpointSetup(c => 75 | { 76 | var topology = TopicTopology.Default; 77 | var endpointName = Conventions.EndpointNamingConvention(typeof(Subscriber)); 78 | topology.OverrideSubscriptionNameFor(endpointName, endpointName.Shorten()); 79 | 80 | topology.SubscribeTo(typeof(MyEvent1).ToTopicName()); 81 | topology.SubscribeTo(typeof(MyEvent2).ToTopicName()); 82 | 83 | c.ConfigureTransport().Topology = topology; 84 | }, metadata => 85 | { 86 | metadata.RegisterPublisherFor(); 87 | metadata.RegisterPublisherFor(); 88 | metadata.RegisterPublisherFor("not-used"); 89 | }); 90 | 91 | public class MyHandler(Context testContext) : IHandleMessages 92 | { 93 | public Task Handle(IMyEvent messageThatIsEnlisted, IMessageHandlerContext context) 94 | { 95 | testContext.AddTrace($"Got event '{messageThatIsEnlisted}'"); 96 | switch (messageThatIsEnlisted) 97 | { 98 | case MyEvent1: 99 | testContext.SubscriberGotMyEvent1 = true; 100 | break; 101 | case MyEvent2: 102 | testContext.SubscriberGotMyEvent2 = true; 103 | break; 104 | default: 105 | break; 106 | } 107 | 108 | return Task.CompletedTask; 109 | } 110 | } 111 | } 112 | 113 | public class MyEvent1 : IMyEvent; 114 | 115 | public class MyEvent2 : IMyEvent; 116 | 117 | public interface IMyEvent : IEvent; 118 | } 119 | } -------------------------------------------------------------------------------- /src/AcceptanceTests/TestIndependenceMutator.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.AcceptanceTests 2 | { 3 | using System.Threading.Tasks; 4 | using AcceptanceTesting; 5 | using MessageMutator; 6 | 7 | class TestIndependenceMutator : IMutateOutgoingTransportMessages 8 | { 9 | readonly string testRunId; 10 | 11 | public TestIndependenceMutator(ScenarioContext scenarioContext) 12 | { 13 | testRunId = scenarioContext.TestRunId.ToString(); 14 | } 15 | 16 | public Task MutateOutgoing(MutateOutgoingTransportMessageContext context) 17 | { 18 | context.OutgoingHeaders["$AcceptanceTesting.TestRunId"] = testRunId; 19 | return Task.CompletedTask; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/AcceptanceTests/TestIndependenceSkipBehavior.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.AcceptanceTests 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using AcceptanceTesting; 6 | using NUnit.Framework; 7 | using Pipeline; 8 | 9 | class TestIndependenceSkipBehavior : IBehavior 10 | { 11 | readonly string testRunId; 12 | 13 | public TestIndependenceSkipBehavior(ScenarioContext scenarioContext) 14 | { 15 | testRunId = scenarioContext.TestRunId.ToString(); 16 | } 17 | 18 | public Task Invoke(ITransportReceiveContext context, Func next) 19 | { 20 | if (context.Message.Headers.TryGetValue("$AcceptanceTesting.TestRunId", out var runId) && runId != testRunId) 21 | { 22 | TestContext.Out.WriteLine($"Skipping message {context.Message.MessageId} from previous test run"); 23 | return Task.CompletedTask; 24 | } 25 | 26 | return next(context); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/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 => true; 10 | 11 | public bool SupportsNativePubSub => true; 12 | 13 | public bool SupportsDelayedDelivery => true; 14 | 15 | public bool SupportsOutbox => true; 16 | 17 | public bool SupportsPurgeOnStartup => false; 18 | 19 | public IConfigureEndpointTestExecution CreateTransportConfiguration() => new ConfigureEndpointAzureServiceBusTransport(); 20 | 21 | public IConfigureEndpointTestExecution CreatePersistenceConfiguration() => new ConfigureEndpointAcceptanceTestingPersistence(); 22 | } 23 | } -------------------------------------------------------------------------------- /src/AcceptanceTests/When_loading_from_options.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.AcceptanceTests.Receiving 2 | { 3 | using System; 4 | using System.Text.Json; 5 | using System.Threading.Tasks; 6 | using AcceptanceTesting; 7 | using Azure.Messaging.ServiceBus; 8 | using Azure.Messaging.ServiceBus.Administration; 9 | using NServiceBus.AcceptanceTests.EndpointTemplates; 10 | using NUnit.Framework; 11 | using Conventions = NServiceBus.AcceptanceTesting.Customization.Conventions; 12 | 13 | public class When_loading_from_options 14 | { 15 | string TopicName; 16 | 17 | [SetUp] 18 | public async Task Setup() 19 | { 20 | TopicName = "PublisherFromOptions"; 21 | 22 | var adminClient = 23 | new ServiceBusAdministrationClient( 24 | Environment.GetEnvironmentVariable("AzureServiceBus_ConnectionString")); 25 | try 26 | { 27 | // makes sure during local development the topic gets cleared before each test run 28 | await adminClient.DeleteTopicAsync(TopicName); 29 | } 30 | catch (ServiceBusException ex) when (ex.Reason == ServiceBusFailureReason.MessagingEntityNotFound) 31 | { 32 | } 33 | } 34 | 35 | [Test] 36 | public async Task Should_allow_topic_per_event_type_options() => 37 | await Scenario.Define() 38 | .WithEndpoint(b => 39 | { 40 | b.CustomConfig(c => 41 | { 42 | var transport = c.ConfigureTransport(); 43 | // doing a deliberate roundtrip to ensure that the options are correctly serialized and deserialized 44 | var serializedOptions = JsonSerializer.Serialize(new TopologyOptions 45 | { 46 | QueueNameToSubscriptionNameMap = { { Conventions.EndpointNamingConvention(typeof(Publisher)), TopicName } }, 47 | PublishedEventToTopicsMap = { { typeof(Event).FullName, TopicName } }, 48 | SubscribedEventToTopicsMap = { { typeof(Event).FullName, [TopicName] } } 49 | }, TopologyOptionsSerializationContext.Default.TopologyOptions); 50 | var options = JsonSerializer.Deserialize(serializedOptions, TopologyOptionsSerializationContext.Default.TopologyOptions); 51 | transport.Topology = TopicTopology.FromOptions(options); 52 | }); 53 | b.When((session, c) => session.Publish(new Event())); 54 | }) 55 | .Done(c => c.EventReceived) 56 | .Run(); 57 | 58 | [Test] 59 | public async Task Should_allow_migration_options() => 60 | await Scenario.Define() 61 | .WithEndpoint(b => 62 | { 63 | b.CustomConfig(c => 64 | { 65 | var transport = c.ConfigureTransport(); 66 | // doing a deliberate roundtrip to ensure that the options are correctly serialized and deserialized 67 | var serializedOptions = JsonSerializer.Serialize(new MigrationTopologyOptions 68 | { 69 | QueueNameToSubscriptionNameMap = { { Conventions.EndpointNamingConvention(typeof(Publisher)), TopicName } }, 70 | SubscribedEventToRuleNameMap = { { typeof(Event).FullName, typeof(Event).FullName.Shorten() } }, 71 | TopicToPublishTo = TopicName, 72 | TopicToSubscribeOn = TopicName, 73 | EventsToMigrateMap = [typeof(Event).FullName] 74 | }, TopologyOptionsSerializationContext.Default.TopologyOptions); 75 | var options = JsonSerializer.Deserialize(serializedOptions, TopologyOptionsSerializationContext.Default.TopologyOptions); 76 | var topology = (MigrationTopology)TopicTopology.FromOptions(options); 77 | transport.Topology = topology; 78 | }); 79 | b.When((session, c) => session.Publish(new Event())); 80 | }) 81 | .Done(c => c.EventReceived) 82 | .Run(); 83 | 84 | public class Context : ScenarioContext 85 | { 86 | public bool EventReceived { get; set; } 87 | } 88 | 89 | public class Publisher : EndpointConfigurationBuilder 90 | { 91 | public Publisher() => 92 | EndpointSetup(b => { }, metadata => 93 | { 94 | metadata.RegisterSelfAsPublisherFor(this); 95 | }); 96 | 97 | public class Handler(Context testContext) : IHandleMessages 98 | { 99 | public Task Handle(Event request, IMessageHandlerContext context) 100 | { 101 | testContext.EventReceived = true; 102 | 103 | return Task.CompletedTask; 104 | } 105 | } 106 | } 107 | 108 | public class Event : IEvent; 109 | } 110 | } -------------------------------------------------------------------------------- /src/AcceptanceTests/When_operating_with_least_privilege.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.AcceptanceTests 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using AcceptanceTesting; 6 | using Azure.Messaging.ServiceBus; 7 | using Azure.Messaging.ServiceBus.Administration; 8 | using Features; 9 | using NServiceBus.AcceptanceTests; 10 | using NServiceBus.AcceptanceTests.EndpointTemplates; 11 | using NServiceBus.Configuration.AdvancedExtensibility; 12 | using NUnit.Framework; 13 | 14 | public class When_operating_with_least_privilege : NServiceBusAcceptanceTest 15 | { 16 | const string DedicatedTopic = "bundle-no-manage-rights"; 17 | 18 | [SetUp] 19 | public async Task Setup() 20 | { 21 | var adminClient = 22 | new ServiceBusAdministrationClient( 23 | Environment.GetEnvironmentVariable("AzureServiceBus_ConnectionString")); 24 | try 25 | { 26 | // makes sure during local development the topic gets cleared before each test run 27 | await adminClient.DeleteTopicAsync(DedicatedTopic); 28 | } 29 | catch (ServiceBusException ex) when (ex.Reason == ServiceBusFailureReason.MessagingEntityNotFound) 30 | { 31 | } 32 | } 33 | 34 | [Test] 35 | public async Task Should_allow_message_operations_on_existing_resources() 36 | { 37 | // Run the scenario first with manage rights to make sure the topic and the subscription is created 38 | await Scenario.Define() 39 | .WithEndpoint() 40 | .WithEndpoint() 41 | .Done(c => c.EndpointsStarted) 42 | .Run(); 43 | 44 | // Now we run without manage rights 45 | var context = await Scenario.Define() 46 | .WithEndpoint(b => 47 | { 48 | b.CustomConfig(c => 49 | { 50 | // least-privilege mode doesn't support installers 51 | c.GetSettings().Set("Installers.Enable", false); 52 | // AutoSubscribe is not supported in least-privilege mode 53 | c.DisableFeature(); 54 | 55 | var transport = c.ConfigureTransport(); 56 | transport.ConnectionString = 57 | Environment.GetEnvironmentVariable("AzureServiceBus_ConnectionString_Restricted"); 58 | }); 59 | b.When(session => session.SendLocal(new MyCommand())); 60 | }) 61 | .WithEndpoint(b => 62 | { 63 | b.CustomConfig(c => 64 | { 65 | // least-privilege mode doesn't support installers 66 | c.GetSettings().Set("Installers.Enable", false); 67 | // AutoSubscribe is not supported in least-privilege mode 68 | c.DisableFeature(); 69 | 70 | var transport = c.ConfigureTransport(); 71 | transport.ConnectionString = 72 | Environment.GetEnvironmentVariable("AzureServiceBus_ConnectionString_Restricted"); 73 | }); 74 | }) 75 | .Done(c => c.SubscriberGotEvent) 76 | .Run(); 77 | 78 | Assert.That(context.SubscriberGotEvent, Is.True); 79 | } 80 | 81 | public class Context : ScenarioContext 82 | { 83 | public bool SubscriberGotEvent { get; set; } 84 | } 85 | 86 | public class Publisher : EndpointConfigurationBuilder 87 | { 88 | public Publisher() => 89 | EndpointSetup(b => 90 | { 91 | }, metadata => metadata.RegisterSelfAsPublisherFor(this)); 92 | 93 | public class MyHandler : IHandleMessages 94 | { 95 | public Task Handle(MyCommand message, IMessageHandlerContext context) 96 | => context.Publish(new MyEvent()); 97 | } 98 | } 99 | 100 | public class Subscriber : EndpointConfigurationBuilder 101 | { 102 | public Subscriber() 103 | => EndpointSetup(b => 104 | { 105 | }, metadata => metadata.RegisterPublisherFor()); 106 | 107 | public class MyHandler : IHandleMessages 108 | { 109 | public MyHandler(Context testContext) => this.testContext = testContext; 110 | 111 | public Task Handle(MyEvent message, IMessageHandlerContext context) 112 | { 113 | testContext.SubscriberGotEvent = true; 114 | return Task.CompletedTask; 115 | } 116 | 117 | readonly Context testContext; 118 | } 119 | } 120 | 121 | public class MyEvent : IEvent 122 | { 123 | } 124 | 125 | public class MyCommand : ICommand 126 | { 127 | } 128 | } 129 | } -------------------------------------------------------------------------------- /src/AcceptanceTests/When_using_token_credential_with_fully_qualified_namespace.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.AcceptanceTests 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using AcceptanceTesting; 6 | using Azure.Identity; 7 | using Azure.Messaging.ServiceBus; 8 | using NServiceBus.AcceptanceTests; 9 | using NServiceBus.AcceptanceTests.EndpointTemplates; 10 | using NUnit.Framework; 11 | 12 | public class When_using_token_credential_with_fully_qualified_namespace : NServiceBusAcceptanceTest 13 | { 14 | string fullyQualifiedNamespace; 15 | 16 | [SetUp] 17 | public void Setup() 18 | { 19 | var connectionString = Environment.GetEnvironmentVariable("AzureServiceBus_ConnectionString"); 20 | var connectionStringProperties = ServiceBusConnectionStringProperties.Parse(connectionString); 21 | fullyQualifiedNamespace = connectionStringProperties.FullyQualifiedNamespace; 22 | } 23 | 24 | [Test] 25 | public async Task Should_work() 26 | { 27 | var context = await Scenario.Define() 28 | .WithEndpoint(b => 29 | { 30 | b.CustomConfig(c => 31 | { 32 | var transport = c.ConfigureTransport(); 33 | transport.FullyQualifiedNamespace = fullyQualifiedNamespace; 34 | transport.TokenCredential = new DefaultAzureCredential(); 35 | }); 36 | b.When(session => session.SendLocal(new MyCommand())); 37 | }) 38 | .WithEndpoint(b => 39 | { 40 | b.CustomConfig(c => 41 | { 42 | var transport = c.ConfigureTransport(); 43 | transport.FullyQualifiedNamespace = fullyQualifiedNamespace; 44 | transport.TokenCredential = new DefaultAzureCredential(); 45 | }); 46 | }) 47 | .Done(c => c.SubscriberGotEvent) 48 | .Run(); 49 | 50 | Assert.That(context.SubscriberGotEvent, Is.True); 51 | } 52 | 53 | public class Context : ScenarioContext 54 | { 55 | public bool SubscriberGotEvent { get; set; } 56 | } 57 | 58 | public class Publisher : EndpointConfigurationBuilder 59 | { 60 | public Publisher() 61 | => EndpointSetup(_ => { }, 62 | metadata => metadata.RegisterSelfAsPublisherFor(this)); 63 | 64 | public class MyHandler : IHandleMessages 65 | { 66 | public Task Handle(MyCommand message, IMessageHandlerContext context) 67 | => context.Publish(new MyEvent()); 68 | } 69 | } 70 | 71 | public class Subscriber : EndpointConfigurationBuilder 72 | { 73 | public Subscriber() => EndpointSetup(_ => { }, 74 | metadata => metadata.RegisterPublisherFor()); 75 | 76 | public class MyHandler(Context testContext) : IHandleMessages 77 | { 78 | public Task Handle(MyEvent message, IMessageHandlerContext context) 79 | { 80 | testContext.SubscriberGotEvent = true; 81 | return Task.CompletedTask; 82 | } 83 | } 84 | } 85 | 86 | public class MyEvent : IEvent; 87 | 88 | public class MyCommand : ICommand; 89 | } 90 | } -------------------------------------------------------------------------------- /src/CommandLine/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # Justification: Application synchronization contexts don't require ConfigureAwait(false) 4 | dotnet_diagnostic.CA2007.severity = none 5 | 6 | # Justification: Application may not need cancellation support 7 | dotnet_diagnostic.PS0018.severity = suggestion 8 | dotnet_diagnostic.PS0013.severity = suggestion 9 | -------------------------------------------------------------------------------- /src/CommandLine/CommandRunner.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.CommandLine 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using Azure.Identity; 6 | using Azure.Messaging.ServiceBus.Administration; 7 | using McMaster.Extensions.CommandLineUtils; 8 | 9 | static class CommandRunner 10 | { 11 | public static async Task Run(CommandOption connectionString, CommandOption fullyQualifiedNamespace, Func func) 12 | { 13 | ServiceBusAdministrationClient client; 14 | if (fullyQualifiedNamespace.HasValue()) 15 | { 16 | client = new ServiceBusAdministrationClient(fullyQualifiedNamespace.Value(), new DefaultAzureCredential()); 17 | } 18 | else 19 | { 20 | var connectionStringToUse = connectionString.HasValue() ? connectionString.Value() : Environment.GetEnvironmentVariable(EnvironmentVariableName); 21 | client = new ServiceBusAdministrationClient(connectionStringToUse); 22 | } 23 | await func(client); 24 | } 25 | 26 | public const string EnvironmentVariableName = "AzureServiceBus_ConnectionString"; 27 | } 28 | } -------------------------------------------------------------------------------- /src/CommandLine/NServiceBus.Transport.AzureServiceBus.CommandLine.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | Exe 6 | asb-transport 7 | True 8 | .NET Core global tool to manage Azure Service Bus entities for NServiceBus endpoints 9 | false 10 | Major 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/CommandLine/Queue.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.CommandLine 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using Azure.Messaging.ServiceBus.Administration; 6 | using McMaster.Extensions.CommandLineUtils; 7 | 8 | static class Queue 9 | { 10 | public static Task Create(ServiceBusAdministrationClient client, CommandArgument name, CommandOption size, CommandOption partitioning) 11 | { 12 | var queueDescription = new CreateQueueOptions(name.Value) 13 | { 14 | EnableBatchedOperations = true, 15 | LockDuration = TimeSpan.FromMinutes(5), 16 | MaxDeliveryCount = int.MaxValue, 17 | MaxSizeInMegabytes = (size.HasValue() ? size.ParsedValue : 5) * 1024, 18 | EnablePartitioning = partitioning.HasValue() 19 | }; 20 | 21 | return client.CreateQueueAsync(queueDescription); 22 | } 23 | 24 | public static Task Delete(ServiceBusAdministrationClient client, CommandArgument name) 25 | { 26 | return client.DeleteQueueAsync(name.Value); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/CommandLine/Rule.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.CommandLine 2 | { 3 | using System.Threading.Tasks; 4 | using Azure.Messaging.ServiceBus.Administration; 5 | using McMaster.Extensions.CommandLineUtils; 6 | 7 | static class Rule 8 | { 9 | public static Task Create(ServiceBusAdministrationClient client, CommandArgument endpointName, CommandOption topicName, CommandOption subscriptionName, CommandArgument eventType, CommandOption ruleName) 10 | { 11 | var topicNameToUse = topicName.HasValue() ? topicName.Value() : Topic.DefaultTopicName; 12 | var subscriptionNameToUse = subscriptionName.HasValue() ? subscriptionName.Value() : endpointName.Value; 13 | var eventToSubscribeTo = eventType.Value; 14 | var ruleNameToUse = ruleName.HasValue() ? ruleName.Value() : eventToSubscribeTo; 15 | var description = new CreateRuleOptions(ruleNameToUse, new SqlRuleFilter($"[NServiceBus.EnclosedMessageTypes] LIKE '%{eventToSubscribeTo}%'")); 16 | 17 | return client.CreateRuleAsync(topicNameToUse, subscriptionNameToUse, description); 18 | } 19 | 20 | public static Task Delete(ServiceBusAdministrationClient client, CommandArgument endpointName, CommandOption topicName, CommandOption subscriptionName, CommandArgument eventType, CommandOption ruleName) 21 | { 22 | var topicNameToUse = topicName.HasValue() ? topicName.Value() : Topic.DefaultTopicName; 23 | var subscriptionNameToUse = subscriptionName.HasValue() ? subscriptionName.Value() : endpointName.Value; 24 | var eventToSubscribeTo = eventType.Value; 25 | var ruleNameToUse = ruleName.HasValue() ? ruleName.Value() : eventToSubscribeTo; 26 | 27 | return client.DeleteRuleAsync(topicNameToUse, subscriptionNameToUse, ruleNameToUse); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /src/CommandLine/Subscription.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.CommandLine 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using Azure.Messaging.ServiceBus.Administration; 6 | using McMaster.Extensions.CommandLineUtils; 7 | 8 | static class Subscription 9 | { 10 | public static Task CreateWithRejectAll(ServiceBusAdministrationClient client, CommandArgument endpointName, CommandOption topicName, CommandOption subscriptionName) 11 | { 12 | var topicNameToUse = topicName.HasValue() ? topicName.Value() : Topic.DefaultTopicName; 13 | var subscriptionNameToUse = subscriptionName.HasValue() ? subscriptionName.Value() : endpointName.Value; 14 | 15 | var options = new CreateSubscriptionOptions(topicNameToUse, subscriptionNameToUse) 16 | { 17 | LockDuration = TimeSpan.FromMinutes(5), 18 | ForwardTo = endpointName.Value, 19 | EnableDeadLetteringOnFilterEvaluationExceptions = false, 20 | MaxDeliveryCount = int.MaxValue, 21 | EnableBatchedOperations = true, 22 | UserMetadata = endpointName.Value 23 | }; 24 | 25 | return client.CreateSubscriptionAsync(options, new CreateRuleOptions("$default", new FalseRuleFilter())); 26 | } 27 | 28 | public static Task CreateWithMatchAll(ServiceBusAdministrationClient client, CommandArgument endpointName, CommandArgument topicName, CommandOption subscriptionName) 29 | { 30 | var subscriptionNameToUse = subscriptionName.HasValue() ? subscriptionName.Value() : endpointName.Value; 31 | 32 | var options = new CreateSubscriptionOptions(topicName.Value, subscriptionNameToUse) 33 | { 34 | LockDuration = TimeSpan.FromMinutes(5), 35 | ForwardTo = endpointName.Value, 36 | EnableDeadLetteringOnFilterEvaluationExceptions = false, 37 | MaxDeliveryCount = int.MaxValue, 38 | EnableBatchedOperations = true, 39 | UserMetadata = endpointName.Value 40 | }; 41 | 42 | return client.CreateSubscriptionAsync(options, new CreateRuleOptions("$default", new TrueRuleFilter())); 43 | } 44 | 45 | public static Task CreateForwarding(ServiceBusAdministrationClient client, CommandOption topicToPublishTo, CommandOption topicToSubscribeTo, string subscriptionName) 46 | { 47 | var options = new CreateSubscriptionOptions(topicToPublishTo.Value(), subscriptionName) 48 | { 49 | LockDuration = TimeSpan.FromMinutes(5), 50 | ForwardTo = topicToSubscribeTo.Value(), 51 | EnableDeadLetteringOnFilterEvaluationExceptions = false, 52 | MaxDeliveryCount = int.MaxValue, 53 | EnableBatchedOperations = true, 54 | UserMetadata = topicToSubscribeTo.Value() 55 | }; 56 | 57 | return client.CreateSubscriptionAsync(options, new CreateRuleOptions("$default", new TrueRuleFilter())); 58 | } 59 | 60 | public static Task Delete(ServiceBusAdministrationClient client, CommandArgument endpointName, 61 | CommandArgument topicName, CommandOption subscriptionName) 62 | { 63 | var subscriptionNameToUse = subscriptionName.HasValue() ? subscriptionName.Value() : endpointName.Value; 64 | return client.DeleteSubscriptionAsync(topicName.Value, subscriptionNameToUse); 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/CommandLine/Topic.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.CommandLine 2 | { 3 | using System.Threading.Tasks; 4 | using Azure.Messaging.ServiceBus.Administration; 5 | using McMaster.Extensions.CommandLineUtils; 6 | 7 | static class Topic 8 | { 9 | public static Task Create(ServiceBusAdministrationClient client, CommandOption topicNameToUse, CommandOption size, 10 | CommandOption partitioning) => 11 | Create(client, topicNameToUse.Value(), size, partitioning); 12 | 13 | public static Task Create(ServiceBusAdministrationClient client, CommandArgument topicNameToUse, CommandOption size, 14 | CommandOption partitioning) => 15 | Create(client, topicNameToUse.Value, size, partitioning); 16 | 17 | static Task Create(ServiceBusAdministrationClient client, string topicNameToUse, CommandOption size, CommandOption partitioning) 18 | { 19 | var options = new CreateTopicOptions(topicNameToUse) 20 | { 21 | EnableBatchedOperations = true, 22 | EnablePartitioning = partitioning.HasValue(), 23 | MaxSizeInMegabytes = (size.HasValue() ? size.ParsedValue : 5) * 1024 24 | }; 25 | 26 | return client.CreateTopicAsync(options); 27 | } 28 | 29 | public const string DefaultTopicName = "bundle-1"; 30 | } 31 | } -------------------------------------------------------------------------------- /src/CommandLine/TopicPerEventTopologyEndpoint.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.CommandLine; 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using Azure.Messaging.ServiceBus; 6 | using Azure.Messaging.ServiceBus.Administration; 7 | using McMaster.Extensions.CommandLineUtils; 8 | 9 | static class TopicPerEventTopologyEndpoint 10 | { 11 | public static async Task Create(ServiceBusAdministrationClient client, CommandArgument name, CommandOption size, CommandOption partitioning) 12 | { 13 | try 14 | { 15 | await Queue.Create(client, name, size, partitioning); 16 | } 17 | catch (ServiceBusException ex) when (ex.Reason == ServiceBusFailureReason.MessagingEntityAlreadyExists) 18 | { 19 | Console.WriteLine($"Queue '{name.Value}' already exists, skipping creation"); 20 | } 21 | } 22 | 23 | public static async Task Subscribe(ServiceBusAdministrationClient client, CommandArgument name, CommandArgument topicName, CommandOption subscriptionName, CommandOption size, CommandOption partitioning) 24 | { 25 | try 26 | { 27 | await Topic.Create(client, topicName, size, partitioning); 28 | } 29 | catch (ServiceBusException ex) when (ex.Reason == ServiceBusFailureReason.MessagingEntityAlreadyExists) 30 | { 31 | Console.WriteLine($"Topic '{topicName.Value}' already exists, skipping creation"); 32 | } 33 | 34 | try 35 | { 36 | await Subscription.CreateWithMatchAll(client, name, topicName, subscriptionName); 37 | } 38 | catch (ServiceBusException ex) when (ex.Reason == ServiceBusFailureReason.MessagingEntityAlreadyExists) 39 | { 40 | Console.WriteLine($"Subscription '{name.Value}' already exists, skipping creation"); 41 | } 42 | } 43 | 44 | public static async Task Unsubscribe(ServiceBusAdministrationClient client, CommandArgument name, CommandArgument topicName, CommandOption subscriptionName) 45 | { 46 | try 47 | { 48 | await Subscription.Delete(client, name, topicName, subscriptionName); 49 | } 50 | catch (ServiceBusException ex) when (ex.Reason == ServiceBusFailureReason.MessagingEntityAlreadyExists) 51 | { 52 | Console.WriteLine($"Subscription '{name.Value}' already exists, skipping creation"); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/CommandLineTests/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # Justification: Test project 4 | dotnet_diagnostic.CA2007.severity = none 5 | 6 | # Justification: Cancellation may not be needed in test project 7 | dotnet_diagnostic.PS0018.severity = suggestion 8 | dotnet_diagnostic.PS0013.severity = suggestion 9 | 10 | # Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken 11 | dotnet_diagnostic.NSB0002.severity = suggestion 12 | -------------------------------------------------------------------------------- /src/CommandLineTests/NServiceBus.Transport.AzureServiceBus.CommandLine.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Custom.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5.0 5 | minor 6 | 7 | 8 | -------------------------------------------------------------------------------- /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/MigrationAcceptanceTests/.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | # Justification: Test project 4 | dotnet_diagnostic.CA2007.severity = none 5 | 6 | # Justification: Cancellation may not be needed in test project 7 | dotnet_diagnostic.PS0018.severity = suggestion 8 | dotnet_diagnostic.PS0013.severity = suggestion 9 | 10 | # Justification: Tests don't support cancellation and don't need to forward IMessageHandlerContext.CancellationToken 11 | dotnet_diagnostic.NSB0002.severity = suggestion 12 | -------------------------------------------------------------------------------- /src/MigrationAcceptanceTests/ConfigureEndpointAzureServiceBusTransport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using NServiceBus; 6 | using NServiceBus.AcceptanceTesting.Customization; 7 | using NServiceBus.AcceptanceTesting.Support; 8 | using NServiceBus.MessageMutator; 9 | using NServiceBus.Transport.AzureServiceBus.AcceptanceTests; 10 | 11 | public class ConfigureEndpointAzureServiceBusTransport : IConfigureEndpointTestExecution 12 | { 13 | public Task Configure(string endpointName, EndpointConfiguration configuration, RunSettings settings, PublisherMetadata publisherMetadata) 14 | { 15 | var connectionString = Environment.GetEnvironmentVariable("AzureServiceBus_ConnectionString"); 16 | 17 | if (string.IsNullOrEmpty(connectionString)) 18 | { 19 | throw new InvalidOperationException("envvar AzureServiceBus_ConnectionString not set"); 20 | } 21 | 22 | var topology = TopicTopology.MigrateFromSingleDefaultTopic(); 23 | topology.OverrideSubscriptionNameFor(endpointName, endpointName.Shorten()); 24 | 25 | foreach (var eventType in publisherMetadata.Publishers.SelectMany(p => p.Events)) 26 | { 27 | topology.EventToMigrate(eventType, ruleNameOverride: eventType.FullName.Shorten()); 28 | } 29 | 30 | var transport = new AzureServiceBusTransport(connectionString, topology); 31 | 32 | configuration.UseTransport(transport); 33 | 34 | configuration.RegisterComponents(c => c.AddSingleton()); 35 | configuration.Pipeline.Register("TestIndependenceBehavior", typeof(TestIndependenceSkipBehavior), "Skips messages not created during the current test."); 36 | 37 | configuration.EnforcePublisherMetadataRegistration(endpointName, publisherMetadata); 38 | 39 | return Task.CompletedTask; 40 | } 41 | 42 | public Task Cleanup() => Task.CompletedTask; 43 | } 44 | -------------------------------------------------------------------------------- /src/MigrationAcceptanceTests/NServiceBus.Transport.AzureServiceBus.Migration.AcceptanceTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.0 5 | true 6 | ..\NServiceBusTests.snk 7 | NServiceBus.Transport.AzureServiceBus.Topic.AcceptanceTests 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | TestIndependenceMutator.cs 31 | 32 | 33 | TestIndependenceSkipBehavior.cs 34 | 35 | 36 | TestSuiteConstraints.cs 37 | 38 | 39 | AcceptanceTestExtensions.cs 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/NServiceBus.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Particular/NServiceBus.Transport.AzureServiceBus/b06aff67188f814ed4034e010008a14c97fc057b/src/NServiceBus.snk -------------------------------------------------------------------------------- /src/NServiceBusTests.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Particular/NServiceBus.Transport.AzureServiceBus/b06aff67188f814ed4034e010008a14c97fc057b/src/NServiceBusTests.snk -------------------------------------------------------------------------------- /src/Tests/.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/Tests/APIApprovals.cs: -------------------------------------------------------------------------------- 1 | using NServiceBus; 2 | using NUnit.Framework; 3 | using Particular.Approvals; 4 | using PublicApiGenerator; 5 | 6 | [TestFixture] 7 | public class APIApprovals 8 | { 9 | [Test] 10 | public void Approve() 11 | { 12 | var publicApi = typeof(AzureServiceBusTransport).Assembly.GeneratePublicApi(new ApiGeneratorOptions 13 | { 14 | ExcludeAttributes = ["System.Runtime.Versioning.TargetFrameworkAttribute", "System.Reflection.AssemblyMetadataAttribute"] 15 | }); 16 | Approver.Verify(publicApi); 17 | } 18 | } -------------------------------------------------------------------------------- /src/Tests/ApprovalFiles/MigrationTopologyCreatorTests.Should_create_default_single_topic_topology.approved.txt: -------------------------------------------------------------------------------- 1 | CreateTopicOptions: { 2 | "DefaultMessageTimeToLive": "10675199.02:48:05.4775807", 3 | "AutoDeleteOnIdle": "10675199.02:48:05.4775807", 4 | "MaxSizeInMegabytes": 5120, 5 | "RequiresDuplicateDetection": false, 6 | "DuplicateDetectionHistoryTimeWindow": "00:01:00", 7 | "Name": "bundle-1", 8 | "AuthorizationRules": [], 9 | "Status": {}, 10 | "EnablePartitioning": false, 11 | "SupportOrdering": false, 12 | "EnableBatchedOperations": true, 13 | "UserMetadata": null, 14 | "MaxMessageSizeInKilobytes": null 15 | } 16 | -------------------------------------------------------------------------------- /src/Tests/ApprovalFiles/MigrationTopologyCreatorTests.Should_create_single_topic_topology.approved.txt: -------------------------------------------------------------------------------- 1 | CreateTopicOptions: { 2 | "DefaultMessageTimeToLive": "10675199.02:48:05.4775807", 3 | "AutoDeleteOnIdle": "10675199.02:48:05.4775807", 4 | "MaxSizeInMegabytes": 5120, 5 | "RequiresDuplicateDetection": false, 6 | "DuplicateDetectionHistoryTimeWindow": "00:01:00", 7 | "Name": "bundle-1", 8 | "AuthorizationRules": [], 9 | "Status": {}, 10 | "EnablePartitioning": false, 11 | "SupportOrdering": false, 12 | "EnableBatchedOperations": true, 13 | "UserMetadata": null, 14 | "MaxMessageSizeInKilobytes": null 15 | } 16 | -------------------------------------------------------------------------------- /src/Tests/ApprovalFiles/MigrationTopologyCreatorTests.Should_hierarchy.approved.txt: -------------------------------------------------------------------------------- 1 | CreateTopicOptions: { 2 | "DefaultMessageTimeToLive": "10675199.02:48:05.4775807", 3 | "AutoDeleteOnIdle": "10675199.02:48:05.4775807", 4 | "MaxSizeInMegabytes": 5120, 5 | "RequiresDuplicateDetection": false, 6 | "DuplicateDetectionHistoryTimeWindow": "00:01:00", 7 | "Name": "bundle-1", 8 | "AuthorizationRules": [], 9 | "Status": {}, 10 | "EnablePartitioning": false, 11 | "SupportOrdering": false, 12 | "EnableBatchedOperations": true, 13 | "UserMetadata": null, 14 | "MaxMessageSizeInKilobytes": null 15 | } 16 | CreateTopicOptions: { 17 | "DefaultMessageTimeToLive": "10675199.02:48:05.4775807", 18 | "AutoDeleteOnIdle": "10675199.02:48:05.4775807", 19 | "MaxSizeInMegabytes": 5120, 20 | "RequiresDuplicateDetection": false, 21 | "DuplicateDetectionHistoryTimeWindow": "00:01:00", 22 | "Name": "bundle-2", 23 | "AuthorizationRules": [], 24 | "Status": {}, 25 | "EnablePartitioning": false, 26 | "SupportOrdering": false, 27 | "EnableBatchedOperations": true, 28 | "UserMetadata": null, 29 | "MaxMessageSizeInKilobytes": null 30 | } 31 | CreateSubscriptionOptions: { 32 | "LockDuration": "00:05:00", 33 | "RequiresSession": false, 34 | "DefaultMessageTimeToLive": "10675199.02:48:05.4775807", 35 | "AutoDeleteOnIdle": "10675199.02:48:05.4775807", 36 | "DeadLetteringOnMessageExpiration": false, 37 | "EnableDeadLetteringOnFilterEvaluationExceptions": false, 38 | "TopicName": "bundle-1", 39 | "SubscriptionName": "forwardTo-bundle-2", 40 | "MaxDeliveryCount": 2147483647, 41 | "Status": {}, 42 | "ForwardTo": "bundle-2", 43 | "ForwardDeadLetteredMessagesTo": null, 44 | "EnableBatchedOperations": true, 45 | "UserMetadata": "bundle-2" 46 | } 47 | CreateRuleOptions: { 48 | "Filter": { 49 | "filter-type": "true", 50 | "SqlExpression": "1=1", 51 | "Parameters": {} 52 | }, 53 | "Action": null, 54 | "Name": "$default" 55 | } 56 | -------------------------------------------------------------------------------- /src/Tests/ApprovalFiles/MigrationTopologySubscriptionManagerTests.Should_create_topology_for_events_to_migrate.approved.txt: -------------------------------------------------------------------------------- 1 | CreateRuleOptions(topicName: 'SubscribeTopic', subscriptionName: 'MySubscriptionName'): { 2 | "Filter": { 3 | "filter-type": "sql", 4 | "SqlExpression": "[NServiceBus.EnclosedMessageTypes] LIKE \u0027%NServiceBus.Transport.AzureServiceBus.Tests.MigrationTopologySubscriptionManagerTests\u002BMyEvent1%\u0027", 5 | "Parameters": {} 6 | }, 7 | "Action": null, 8 | "Name": "MyRuleName1" 9 | } 10 | CreateRuleOptions(topicName: 'SubscribeTopic', subscriptionName: 'MySubscriptionName'): { 11 | "Filter": { 12 | "filter-type": "sql", 13 | "SqlExpression": "[NServiceBus.EnclosedMessageTypes] LIKE \u0027%NServiceBus.Transport.AzureServiceBus.Tests.MigrationTopologySubscriptionManagerTests\u002BMyEvent2%\u0027", 14 | "Parameters": {} 15 | }, 16 | "Action": null, 17 | "Name": "MyRuleName2" 18 | } 19 | -------------------------------------------------------------------------------- /src/Tests/ApprovalFiles/MigrationTopologySubscriptionManagerTests.Should_create_topology_for_migrated_and_not_migrated_events.approved.txt: -------------------------------------------------------------------------------- 1 | CreateRuleOptions(topicName: 'SubscribeTopic', subscriptionName: 'MySubscriptionName'): { 2 | "Filter": { 3 | "filter-type": "sql", 4 | "SqlExpression": "[NServiceBus.EnclosedMessageTypes] LIKE \u0027%NServiceBus.Transport.AzureServiceBus.Tests.MigrationTopologySubscriptionManagerTests\u002BMyEvent1%\u0027", 5 | "Parameters": {} 6 | }, 7 | "Action": null, 8 | "Name": "MyRuleName" 9 | } 10 | CreateSubscriptionOptions: { 11 | "LockDuration": "00:05:00", 12 | "RequiresSession": false, 13 | "DefaultMessageTimeToLive": "10675199.02:48:05.4775807", 14 | "AutoDeleteOnIdle": "10675199.02:48:05.4775807", 15 | "DeadLetteringOnMessageExpiration": false, 16 | "EnableDeadLetteringOnFilterEvaluationExceptions": false, 17 | "TopicName": "MyTopic1", 18 | "SubscriptionName": "MySubscriptionName", 19 | "MaxDeliveryCount": 2147483647, 20 | "Status": {}, 21 | "ForwardTo": "SubscribingQueue", 22 | "ForwardDeadLetteredMessagesTo": null, 23 | "EnableBatchedOperations": true, 24 | "UserMetadata": "SubscribingQueue" 25 | } 26 | CreateRuleOptions: { 27 | "Filter": { 28 | "filter-type": "true", 29 | "SqlExpression": "1=1", 30 | "Parameters": {} 31 | }, 32 | "Action": null, 33 | "Name": "$Default" 34 | } 35 | CreateSubscriptionOptions: { 36 | "LockDuration": "00:05:00", 37 | "RequiresSession": false, 38 | "DefaultMessageTimeToLive": "10675199.02:48:05.4775807", 39 | "AutoDeleteOnIdle": "10675199.02:48:05.4775807", 40 | "DeadLetteringOnMessageExpiration": false, 41 | "EnableDeadLetteringOnFilterEvaluationExceptions": false, 42 | "TopicName": "MyTopic2", 43 | "SubscriptionName": "MySubscriptionName", 44 | "MaxDeliveryCount": 2147483647, 45 | "Status": {}, 46 | "ForwardTo": "SubscribingQueue", 47 | "ForwardDeadLetteredMessagesTo": null, 48 | "EnableBatchedOperations": true, 49 | "UserMetadata": "SubscribingQueue" 50 | } 51 | CreateRuleOptions: { 52 | "Filter": { 53 | "filter-type": "true", 54 | "SqlExpression": "1=1", 55 | "Parameters": {} 56 | }, 57 | "Action": null, 58 | "Name": "$Default" 59 | } 60 | -------------------------------------------------------------------------------- /src/Tests/ApprovalFiles/MigrationTopologyTests.Should_self_validate.approved.txt: -------------------------------------------------------------------------------- 1 | TopicToPublishTo: The following topic name(s) do not comply with the Azure Service Bus topic limits: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; TopicToSubscribeOn: The following topic name(s) do not comply with the Azure Service Bus topic limits: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; SubscribedEventToRuleNameMap: The following rule name(s) do not comply with the Azure Service Bus rule limits: ggggggggggggggggggggggggggggggggggggggggggggggggggg; PublishedEventToTopicsMap: The following topic name(s) do not comply with the Azure Service Bus topic limits: ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc; SubscribedEventToTopicsMap: The following topic name(s) do not comply with the Azure Service Bus topic limits: ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd, eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee; QueueNameToSubscriptionNameMap: The following subscription name(s) do not comply with the Azure Service Bus subscription limits: fffffffffffffffffffffffffffffffffffffffffffffffffff -------------------------------------------------------------------------------- /src/Tests/ApprovalFiles/MigrationTopologyTests.Should_self_validate_consistency.approved.txt: -------------------------------------------------------------------------------- 1 | EventsToMigrateMap: TopicToPublishTo, PublishedEventToTopicsMap: The topic to publish 'TopicToPublishTo' for 'NServiceBus.Transport.AzureServiceBus.Tests.MigrationTopologyTests+MyEvent' cannot be the sames as the topic to publish to 'TopicToPublishTo' for the migration topology.; TopicToSubscribeOn, SubscribedEventToTopicsMap: The topic to subscribe 'TopicToSubscribeOn' for 'NServiceBus.Transport.AzureServiceBus.Tests.MigrationTopologyTests+MyEvent' cannot be the sames as the topic to subscribe to 'TopicToSubscribeOn' for the migration topology.; SubscribedEventToTopicsMap: Event 'NServiceBus.Transport.AzureServiceBus.Tests.MigrationTopologyTests+MyEventMappedTwice' is in the migration map and in the subscribed event to topics map. An event type cannot be marked for migration and mapped to a topic at the same time. -------------------------------------------------------------------------------- /src/Tests/ApprovalFiles/TopicPerEventSubscriptionManagerTests.Should_create_topology_for_mapped_events.approved.txt: -------------------------------------------------------------------------------- 1 | CreateSubscriptionOptions: { 2 | "LockDuration": "00:05:00", 3 | "RequiresSession": false, 4 | "DefaultMessageTimeToLive": "10675199.02:48:05.4775807", 5 | "AutoDeleteOnIdle": "10675199.02:48:05.4775807", 6 | "DeadLetteringOnMessageExpiration": false, 7 | "EnableDeadLetteringOnFilterEvaluationExceptions": false, 8 | "TopicName": "MyTopic1", 9 | "SubscriptionName": "MySubscriptionName", 10 | "MaxDeliveryCount": 2147483647, 11 | "Status": {}, 12 | "ForwardTo": "SubscribingQueue", 13 | "ForwardDeadLetteredMessagesTo": null, 14 | "EnableBatchedOperations": true, 15 | "UserMetadata": "SubscribingQueue" 16 | } 17 | CreateRuleOptions: { 18 | "Filter": { 19 | "filter-type": "true", 20 | "SqlExpression": "1=1", 21 | "Parameters": {} 22 | }, 23 | "Action": null, 24 | "Name": "$Default" 25 | } 26 | CreateSubscriptionOptions: { 27 | "LockDuration": "00:05:00", 28 | "RequiresSession": false, 29 | "DefaultMessageTimeToLive": "10675199.02:48:05.4775807", 30 | "AutoDeleteOnIdle": "10675199.02:48:05.4775807", 31 | "DeadLetteringOnMessageExpiration": false, 32 | "EnableDeadLetteringOnFilterEvaluationExceptions": false, 33 | "TopicName": "MyTopic2", 34 | "SubscriptionName": "MySubscriptionName", 35 | "MaxDeliveryCount": 2147483647, 36 | "Status": {}, 37 | "ForwardTo": "SubscribingQueue", 38 | "ForwardDeadLetteredMessagesTo": null, 39 | "EnableBatchedOperations": true, 40 | "UserMetadata": "SubscribingQueue" 41 | } 42 | CreateRuleOptions: { 43 | "Filter": { 44 | "filter-type": "true", 45 | "SqlExpression": "1=1", 46 | "Parameters": {} 47 | }, 48 | "Action": null, 49 | "Name": "$Default" 50 | } 51 | CreateSubscriptionOptions: { 52 | "LockDuration": "00:05:00", 53 | "RequiresSession": false, 54 | "DefaultMessageTimeToLive": "10675199.02:48:05.4775807", 55 | "AutoDeleteOnIdle": "10675199.02:48:05.4775807", 56 | "DeadLetteringOnMessageExpiration": false, 57 | "EnableDeadLetteringOnFilterEvaluationExceptions": false, 58 | "TopicName": "MyTopic3", 59 | "SubscriptionName": "MySubscriptionName", 60 | "MaxDeliveryCount": 2147483647, 61 | "Status": {}, 62 | "ForwardTo": "SubscribingQueue", 63 | "ForwardDeadLetteredMessagesTo": null, 64 | "EnableBatchedOperations": true, 65 | "UserMetadata": "SubscribingQueue" 66 | } 67 | CreateRuleOptions: { 68 | "Filter": { 69 | "filter-type": "true", 70 | "SqlExpression": "1=1", 71 | "Parameters": {} 72 | }, 73 | "Action": null, 74 | "Name": "$Default" 75 | } 76 | -------------------------------------------------------------------------------- /src/Tests/ApprovalFiles/TopicPerEventSubscriptionManagerTests.Should_create_topology_for_unmapped_events.approved.txt: -------------------------------------------------------------------------------- 1 | CreateSubscriptionOptions: { 2 | "LockDuration": "00:05:00", 3 | "RequiresSession": false, 4 | "DefaultMessageTimeToLive": "10675199.02:48:05.4775807", 5 | "AutoDeleteOnIdle": "10675199.02:48:05.4775807", 6 | "DeadLetteringOnMessageExpiration": false, 7 | "EnableDeadLetteringOnFilterEvaluationExceptions": false, 8 | "TopicName": "NServiceBus.Transport.AzureServiceBus.Tests.TopicPerEventSubscriptionManagerTests\u002BMyEvent1", 9 | "SubscriptionName": "MySubscriptionName", 10 | "MaxDeliveryCount": 2147483647, 11 | "Status": {}, 12 | "ForwardTo": "SubscribingQueue", 13 | "ForwardDeadLetteredMessagesTo": null, 14 | "EnableBatchedOperations": true, 15 | "UserMetadata": "SubscribingQueue" 16 | } 17 | CreateRuleOptions: { 18 | "Filter": { 19 | "filter-type": "true", 20 | "SqlExpression": "1=1", 21 | "Parameters": {} 22 | }, 23 | "Action": null, 24 | "Name": "$Default" 25 | } 26 | CreateSubscriptionOptions: { 27 | "LockDuration": "00:05:00", 28 | "RequiresSession": false, 29 | "DefaultMessageTimeToLive": "10675199.02:48:05.4775807", 30 | "AutoDeleteOnIdle": "10675199.02:48:05.4775807", 31 | "DeadLetteringOnMessageExpiration": false, 32 | "EnableDeadLetteringOnFilterEvaluationExceptions": false, 33 | "TopicName": "NServiceBus.Transport.AzureServiceBus.Tests.TopicPerEventSubscriptionManagerTests\u002BMyEvent2", 34 | "SubscriptionName": "MySubscriptionName", 35 | "MaxDeliveryCount": 2147483647, 36 | "Status": {}, 37 | "ForwardTo": "SubscribingQueue", 38 | "ForwardDeadLetteredMessagesTo": null, 39 | "EnableBatchedOperations": true, 40 | "UserMetadata": "SubscribingQueue" 41 | } 42 | CreateRuleOptions: { 43 | "Filter": { 44 | "filter-type": "true", 45 | "SqlExpression": "1=1", 46 | "Parameters": {} 47 | }, 48 | "Action": null, 49 | "Name": "$Default" 50 | } 51 | -------------------------------------------------------------------------------- /src/Tests/ApprovalFiles/TopicPerEventTopologyTests.Should_self_validate.approved.txt: -------------------------------------------------------------------------------- 1 | PublishedEventToTopicsMap: The following topic name(s) do not comply with the Azure Service Bus topic limits: ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc; SubscribedEventToTopicsMap: The following topic name(s) do not comply with the Azure Service Bus topic limits: ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd, eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee; QueueNameToSubscriptionNameMap: The following subscription name(s) do not comply with the Azure Service Bus subscription limits: fffffffffffffffffffffffffffffffffffffffffffffffffff -------------------------------------------------------------------------------- /src/Tests/EventRouting/MigrationTopologyCreatorTests.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.Tests; 2 | 3 | using System.Threading.Tasks; 4 | using NUnit.Framework; 5 | using Particular.Approvals; 6 | 7 | [TestFixture] 8 | public class MigrationTopologyCreatorTests 9 | { 10 | [Test] 11 | public async Task Should_create_single_topic_topology() 12 | { 13 | var topology = TopicTopology.MigrateFromNamedSingleTopic("bundle-1"); 14 | var transportSettings = new AzureServiceBusTransport("connection-string", topology); 15 | 16 | var recordingAdministrationClient = new RecordingServiceBusAdministrationClient(); 17 | var creator = new MigrationTopologyCreator(transportSettings); 18 | 19 | await creator.Create(recordingAdministrationClient); 20 | 21 | Approver.Verify(recordingAdministrationClient.ToString()); 22 | } 23 | 24 | [Test] 25 | public async Task Should_create_default_single_topic_topology() 26 | { 27 | var topology = TopicTopology.MigrateFromSingleDefaultTopic(); 28 | var transportSettings = new AzureServiceBusTransport("connection-string", topology); 29 | 30 | var recordingAdministrationClient = new RecordingServiceBusAdministrationClient(); 31 | var creator = new MigrationTopologyCreator(transportSettings); 32 | 33 | await creator.Create(recordingAdministrationClient); 34 | 35 | Approver.Verify(recordingAdministrationClient.ToString()); 36 | } 37 | 38 | [Test] 39 | public async Task Should_hierarchy() 40 | { 41 | var topology = TopicTopology.MigrateFromTopicHierarchy("bundle-1", "bundle-2"); 42 | var transportSettings = new AzureServiceBusTransport("connection-string", topology); 43 | 44 | var recordingAdministrationClient = new RecordingServiceBusAdministrationClient(); 45 | var creator = new MigrationTopologyCreator(transportSettings); 46 | 47 | await creator.Create(recordingAdministrationClient); 48 | 49 | Approver.Verify(recordingAdministrationClient.ToString()); 50 | } 51 | } -------------------------------------------------------------------------------- /src/Tests/EventRouting/MigrationTopologySubscriptionManagerTests.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.Tests; 2 | 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Extensibility; 6 | using NUnit.Framework; 7 | using Particular.Approvals; 8 | using Unicast.Messages; 9 | 10 | [TestFixture] 11 | public class MigrationTopologySubscriptionManagerTests 12 | { 13 | [Test] 14 | public async Task Should_create_topology_for_events_to_migrate() 15 | { 16 | var topologyOptions = new MigrationTopologyOptions 17 | { 18 | TopicToPublishTo = "PublishTopic", 19 | TopicToSubscribeOn = "SubscribeTopic", 20 | EventsToMigrateMap = 21 | { 22 | typeof(MyEvent1).FullName, 23 | typeof(MyEvent2).FullName 24 | }, 25 | QueueNameToSubscriptionNameMap = { { "SubscribingQueue", "MySubscriptionName" } }, 26 | SubscribedEventToRuleNameMap = 27 | { 28 | { typeof(MyEvent1).FullName, "MyRuleName1" }, 29 | { typeof(MyEvent2).FullName, "MyRuleName2" } 30 | } 31 | }; 32 | 33 | var builder = new StringBuilder(); 34 | var client = new RecordingServiceBusClient(builder); 35 | var administrationClient = new RecordingServiceBusAdministrationClient(builder); 36 | 37 | var subscriptionManager = new MigrationTopologySubscriptionManager(new SubscriptionManagerCreationOptions 38 | { 39 | SubscribingQueueName = "SubscribingQueue", 40 | Client = client, 41 | AdministrationClient = administrationClient 42 | }, topologyOptions); 43 | 44 | await subscriptionManager.SubscribeAll([new MessageMetadata(typeof(MyEvent1)), new MessageMetadata(typeof(MyEvent2))], new ContextBag()); 45 | 46 | Approver.Verify(builder.ToString()); 47 | } 48 | 49 | [Test] 50 | public async Task Should_create_topology_for_migrated_and_not_migrated_events() 51 | { 52 | var topologyOptions = new MigrationTopologyOptions 53 | { 54 | TopicToPublishTo = "PublishTopic", 55 | TopicToSubscribeOn = "SubscribeTopic", 56 | EventsToMigrateMap = { typeof(MyEvent1).FullName }, 57 | QueueNameToSubscriptionNameMap = { { "SubscribingQueue", "MySubscriptionName" } }, 58 | SubscribedEventToRuleNameMap = { { typeof(MyEvent1).FullName, "MyRuleName" } }, 59 | SubscribedEventToTopicsMap = { { typeof(MyEvent2).FullName, ["MyTopic1", "MyTopic2"] } } 60 | }; 61 | 62 | var builder = new StringBuilder(); 63 | var client = new RecordingServiceBusClient(builder); 64 | var administrationClient = new RecordingServiceBusAdministrationClient(builder); 65 | 66 | var subscriptionManager = new MigrationTopologySubscriptionManager(new SubscriptionManagerCreationOptions 67 | { 68 | SubscribingQueueName = "SubscribingQueue", 69 | Client = client, 70 | AdministrationClient = administrationClient 71 | }, topologyOptions); 72 | 73 | await subscriptionManager.SubscribeAll([new MessageMetadata(typeof(MyEvent1)), new MessageMetadata(typeof(MyEvent2))], new ContextBag()); 74 | 75 | Approver.Verify(builder.ToString()); 76 | } 77 | 78 | [Test] 79 | public async Task Should_throw_when_event_is_not_mapped() 80 | { 81 | var topologyOptions = new MigrationTopologyOptions 82 | { 83 | TopicToPublishTo = "TopicToPublishTo", 84 | TopicToSubscribeOn = "TopicToSubscribeOn", 85 | }; 86 | 87 | var client = new RecordingServiceBusClient(); 88 | var administrationClient = new RecordingServiceBusAdministrationClient(); 89 | 90 | var subscriptionManager = new MigrationTopologySubscriptionManager(new SubscriptionManagerCreationOptions 91 | { 92 | SubscribingQueueName = "SubscribingQueue", 93 | Client = client, 94 | AdministrationClient = administrationClient 95 | }, topologyOptions); 96 | 97 | await Assert.ThatAsync(() => subscriptionManager.SubscribeAll([new MessageMetadata(typeof(MyEvent1))], new ContextBag()), Throws.Exception); 98 | } 99 | 100 | class MyEvent1; 101 | class MyEvent2; 102 | } -------------------------------------------------------------------------------- /src/Tests/EventRouting/MigrationTopologyTests.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.Tests; 2 | 3 | using System.ComponentModel.DataAnnotations; 4 | using NUnit.Framework; 5 | using Particular.Approvals; 6 | 7 | [TestFixture] 8 | public class MigrationTopologyTests 9 | { 10 | [Test] 11 | public void Should_self_validate() 12 | { 13 | var topologyOptions = new MigrationTopologyOptions 14 | { 15 | TopicToPublishTo = new string('a', 261), 16 | TopicToSubscribeOn = new string('a', 261), 17 | PublishedEventToTopicsMap = 18 | { 19 | { typeof(MyEvent).FullName, new string('c', 261) }, 20 | }, 21 | SubscribedEventToTopicsMap = 22 | { 23 | { typeof(MyEvent).FullName, [new string('d', 261), new string('e', 261)] }, 24 | { typeof(MyEventMappedTwice).FullName, ["MyEventMappedTwice"] } 25 | }, 26 | QueueNameToSubscriptionNameMap = { { "SubscribingQueue", new string('f', 51) } }, 27 | SubscribedEventToRuleNameMap = { { typeof(MyEvent).FullName, new string('g', 51) } }, 28 | }; 29 | 30 | var topology = TopicTopology.FromOptions(topologyOptions); 31 | 32 | var validationException = Assert.Catch(() => topology.Validate()); 33 | 34 | Approver.Verify(validationException.Message); 35 | } 36 | 37 | [Test] 38 | public void Should_self_validate_consistency() 39 | { 40 | var topologyOptions = new MigrationTopologyOptions 41 | { 42 | TopicToPublishTo = "TopicToPublishTo", 43 | TopicToSubscribeOn = "TopicToSubscribeOn", 44 | PublishedEventToTopicsMap = { { typeof(MyEvent).FullName, "TopicToPublishTo" } }, 45 | SubscribedEventToTopicsMap = 46 | { 47 | { typeof(MyEvent).FullName, ["SomeOtherTopic", "TopicToSubscribeOn"] }, 48 | { typeof(MyEventMappedTwice).FullName, ["MyEventMappedTwice"] } 49 | }, 50 | EventsToMigrateMap = { typeof(MyEventMappedTwice).FullName } 51 | }; 52 | 53 | var topology = TopicTopology.FromOptions(topologyOptions); 54 | 55 | ValidationException validationException = Assert.Catch(() => topology.Validate()); 56 | 57 | Approver.Verify(validationException.Message); 58 | } 59 | 60 | // With the generic host validation can already be done at startup and this allows disabling further validation 61 | // for advanced scenarios to save startup time. 62 | [Test] 63 | public void Should_allow_disabling_validation() 64 | { 65 | var topologyOptions = new MigrationTopologyOptions 66 | { 67 | TopicToPublishTo = new string('a', 261), 68 | TopicToSubscribeOn = new string('a', 261) 69 | }; 70 | 71 | var topology = TopicTopology.FromOptions(topologyOptions); 72 | topology.OptionsValidator = new TopologyOptionsDisableValidationValidator(); 73 | 74 | Assert.DoesNotThrow(() => topology.Validate()); 75 | } 76 | 77 | class MyEvent; 78 | class MyEventMappedTwice; 79 | } -------------------------------------------------------------------------------- /src/Tests/EventRouting/TopicPerEventSubscriptionManagerTests.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.Tests; 2 | 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Extensibility; 6 | using NUnit.Framework; 7 | using Particular.Approvals; 8 | using Unicast.Messages; 9 | 10 | [TestFixture] 11 | public class TopicPerEventSubscriptionManagerTests 12 | { 13 | [Test] 14 | public async Task Should_create_topology_for_mapped_events() 15 | { 16 | var topologyOptions = new TopologyOptions 17 | { 18 | SubscribedEventToTopicsMap = 19 | { 20 | { typeof(MyEvent1).FullName, ["MyTopic1", "MyTopic2"] }, 21 | { typeof(MyEvent2).FullName, ["MyTopic3"] } 22 | }, 23 | QueueNameToSubscriptionNameMap = { { "SubscribingQueue", "MySubscriptionName" } }, 24 | }; 25 | 26 | var builder = new StringBuilder(); 27 | var client = new RecordingServiceBusClient(builder); 28 | var administrationClient = new RecordingServiceBusAdministrationClient(builder); 29 | 30 | var subscriptionManager = new TopicPerEventTopologySubscriptionManager(new SubscriptionManagerCreationOptions 31 | { 32 | SubscribingQueueName = "SubscribingQueue", 33 | Client = client, 34 | AdministrationClient = administrationClient 35 | }, topologyOptions); 36 | 37 | await subscriptionManager.SubscribeAll([new MessageMetadata(typeof(MyEvent1)), new MessageMetadata(typeof(MyEvent2))], new ContextBag()); 38 | 39 | Approver.Verify(builder.ToString()); 40 | } 41 | 42 | [Test] 43 | public async Task Should_create_topology_for_unmapped_events() 44 | { 45 | var topologyOptions = new TopologyOptions 46 | { 47 | QueueNameToSubscriptionNameMap = { { "SubscribingQueue", "MySubscriptionName" } }, 48 | }; 49 | 50 | var builder = new StringBuilder(); 51 | var client = new RecordingServiceBusClient(builder); 52 | var administrationClient = new RecordingServiceBusAdministrationClient(builder); 53 | 54 | var subscriptionManager = new TopicPerEventTopologySubscriptionManager(new SubscriptionManagerCreationOptions 55 | { 56 | SubscribingQueueName = "SubscribingQueue", 57 | Client = client, 58 | AdministrationClient = administrationClient 59 | }, topologyOptions); 60 | 61 | await subscriptionManager.SubscribeAll([new MessageMetadata(typeof(MyEvent1)), new MessageMetadata(typeof(MyEvent2))], new ContextBag()); 62 | 63 | Approver.Verify(builder.ToString()); 64 | } 65 | 66 | class MyEvent1; 67 | class MyEvent2; 68 | } -------------------------------------------------------------------------------- /src/Tests/EventRouting/TopicPerEventTopologyTests.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.Tests; 2 | 3 | using System.ComponentModel.DataAnnotations; 4 | using NUnit.Framework; 5 | using Particular.Approvals; 6 | 7 | [TestFixture] 8 | public class TopicPerEventTopologyTests 9 | { 10 | [Test] 11 | public void PublishDestination_Should_return_mapped_topic_when_event_is_mapped() 12 | { 13 | var topologyOptions = new TopologyOptions 14 | { 15 | PublishedEventToTopicsMap = 16 | { 17 | [typeof(MyEvent).FullName] = "MyTopic" 18 | } 19 | }; 20 | 21 | var topology = TopicTopology.FromOptions(topologyOptions); 22 | 23 | var result = topology.GetPublishDestination(typeof(MyEvent)); 24 | 25 | Assert.That(result, Is.EqualTo("MyTopic")); 26 | } 27 | 28 | [Test] 29 | public void Should_self_validate() 30 | { 31 | var topologyOptions = new TopologyOptions 32 | { 33 | PublishedEventToTopicsMap = { { typeof(MyEvent).FullName, new string('c', 261) } }, 34 | SubscribedEventToTopicsMap = { { typeof(MyEvent).FullName, [new string('d', 261), new string('e', 261)] } }, 35 | QueueNameToSubscriptionNameMap = { { "SubscribingQueue", new string('f', 51) } }, 36 | }; 37 | 38 | var topology = TopicTopology.FromOptions(topologyOptions); 39 | 40 | var validationException = Assert.Catch(() => topology.Validate()); 41 | 42 | Approver.Verify(validationException.Message); 43 | } 44 | 45 | // With the generic host validation can already be done at startup and this allows disabling further validation 46 | // for advanced scenarios to save startup time. 47 | [Test] 48 | public void Should_allow_disabling_validation() 49 | { 50 | var topologyOptions = new TopologyOptions 51 | { 52 | PublishedEventToTopicsMap = { { typeof(MyEvent).FullName, new string('c', 261) } } 53 | }; 54 | 55 | var topology = TopicTopology.FromOptions(topologyOptions); 56 | topology.OptionsValidator = new TopologyOptionsDisableValidationValidator(); 57 | 58 | Assert.DoesNotThrow(() => topology.Validate()); 59 | } 60 | 61 | class MyEvent; 62 | } -------------------------------------------------------------------------------- /src/Tests/FakeProcessor.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | 3 | namespace NServiceBus.Transport.AzureServiceBus.Tests 4 | { 5 | using System.Runtime.CompilerServices; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Azure.Messaging.ServiceBus; 9 | 10 | public class FakeProcessor : ServiceBusProcessor 11 | { 12 | public bool WasStarted { get; private set; } 13 | public bool WasStopped { get; private set; } 14 | 15 | public override Task StartProcessingAsync(CancellationToken cancellationToken = new CancellationToken()) 16 | { 17 | WasStarted = true; 18 | return Task.CompletedTask; 19 | } 20 | 21 | public override Task StopProcessingAsync(CancellationToken cancellationToken = new CancellationToken()) 22 | { 23 | WasStopped = true; 24 | return Task.CompletedTask; 25 | } 26 | 27 | public Task ProcessMessage(ServiceBusReceivedMessage message, ServiceBusReceiver? receiver = null, CancellationToken cancellationToken = default) 28 | { 29 | var eventArgs = new CustomProcessMessageEventArgs(message, receiver ?? new FakeReceiver(), cancellationToken); 30 | receivedMessageToEventArgs.Add(message, eventArgs); 31 | return OnProcessMessageAsync(eventArgs); 32 | } 33 | 34 | readonly ConditionalWeakTable 35 | receivedMessageToEventArgs = []; 36 | 37 | sealed class CustomProcessMessageEventArgs : ProcessMessageEventArgs 38 | { 39 | public CustomProcessMessageEventArgs(ServiceBusReceivedMessage message, ServiceBusReceiver receiver, CancellationToken cancellationToken) : base(message, receiver, cancellationToken) 40 | { 41 | } 42 | 43 | public CustomProcessMessageEventArgs(ServiceBusReceivedMessage message, ServiceBusReceiver receiver, string identifier, CancellationToken cancellationToken) : base(message, receiver, identifier, cancellationToken) 44 | { 45 | } 46 | 47 | public Task RaiseMessageLockLost(MessageLockLostEventArgs args, CancellationToken cancellationToken = default) => OnMessageLockLostAsync(args); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /src/Tests/FakeReceiver.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.Tests 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Azure.Messaging.ServiceBus; 8 | 9 | public class FakeReceiver : ServiceBusReceiver 10 | { 11 | readonly List<(ServiceBusReceivedMessage, IDictionary propertiesToModify)> abandonedMessages = []; 12 | readonly List completedMessages = []; 13 | readonly List completingMessages = []; 14 | 15 | public Func CompleteMessageCallback = (_, _) => Task.CompletedTask; 16 | 17 | public IReadOnlyCollection<(ServiceBusReceivedMessage, IDictionary propertiesToModify)> AbandonedMessages 18 | => abandonedMessages; 19 | 20 | public IReadOnlyCollection CompletedMessages 21 | => completedMessages; 22 | 23 | public IReadOnlyCollection CompletingMessages 24 | => completingMessages; 25 | 26 | public override Task AbandonMessageAsync(ServiceBusReceivedMessage message, IDictionary propertiesToModify = null, 27 | CancellationToken cancellationToken = default) 28 | { 29 | abandonedMessages.Add((message, propertiesToModify ?? new Dictionary(0))); 30 | return Task.CompletedTask; 31 | } 32 | 33 | public override async Task CompleteMessageAsync(ServiceBusReceivedMessage message, 34 | CancellationToken cancellationToken = default) 35 | { 36 | completingMessages.Add(message); 37 | await CompleteMessageCallback(message, cancellationToken); 38 | completedMessages.Add(message); 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/Tests/FakeSender.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.Tests 2 | { 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Runtime.CompilerServices; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Azure.Messaging.ServiceBus; 9 | 10 | public class FakeSender : ServiceBusSender 11 | { 12 | readonly List sentMessages = []; 13 | readonly List batchedMessages = []; 14 | readonly ConditionalWeakTable> 15 | batchToBackingStore = 16 | []; 17 | 18 | public IReadOnlyCollection IndividuallySentMessages => sentMessages; 19 | public IReadOnlyCollection BatchSentMessages => batchedMessages; 20 | public Func TryAdd { get; set; } = _ => true; 21 | public Action SendMessageAction { get; set; } = _ => { }; 22 | public Action SendMessageBatchAction { get; set; } = _ => { }; 23 | 24 | public override string FullyQualifiedNamespace { get; } = "FullyQualifiedNamespace"; 25 | 26 | public IReadOnlyCollection this[ServiceBusMessageBatch batch] 27 | { 28 | get => batchToBackingStore.TryGetValue(batch, out var store) ? store : Array.Empty(); 29 | set => throw new NotSupportedException(); 30 | } 31 | 32 | public override ValueTask CreateMessageBatchAsync(CancellationToken cancellationToken = default) 33 | { 34 | var batchMessageStore = new List(); 35 | ServiceBusMessageBatch serviceBusMessageBatch = ServiceBusModelFactory.ServiceBusMessageBatch(256 * 1024, batchMessageStore, tryAddCallback: TryAdd); 36 | batchToBackingStore.Add(serviceBusMessageBatch, batchMessageStore); 37 | return new ValueTask(serviceBusMessageBatch); 38 | } 39 | 40 | public override Task SendMessageAsync(ServiceBusMessage message, CancellationToken cancellationToken = default) 41 | { 42 | cancellationToken.ThrowIfCancellationRequested(); 43 | SendMessageAction(message); 44 | sentMessages.Add(message); 45 | return Task.CompletedTask; 46 | } 47 | 48 | public override Task SendMessagesAsync(ServiceBusMessageBatch messageBatch, CancellationToken cancellationToken = default) 49 | { 50 | cancellationToken.ThrowIfCancellationRequested(); 51 | SendMessageBatchAction(messageBatch); 52 | batchedMessages.Add(messageBatch); 53 | return Task.CompletedTask; 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/Tests/FakeServiceBusClient.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.Tests 2 | { 3 | using System.Collections.Generic; 4 | using Azure.Messaging.ServiceBus; 5 | 6 | public class FakeServiceBusClient : ServiceBusClient 7 | { 8 | public Dictionary Senders { get; } = []; 9 | public Dictionary Processors { get; } = []; 10 | 11 | public override ServiceBusSender CreateSender(string queueOrTopicName) 12 | { 13 | if (!Senders.TryGetValue(queueOrTopicName, out var fakeSender)) 14 | { 15 | fakeSender = new FakeSender(); 16 | Senders.Add(queueOrTopicName, fakeSender); 17 | } 18 | return fakeSender; 19 | } 20 | 21 | public override ServiceBusSender CreateSender(string queueOrTopicName, ServiceBusSenderOptions options) 22 | { 23 | if (!Senders.TryGetValue(queueOrTopicName, out var fakeSender)) 24 | { 25 | fakeSender = new FakeSender(); 26 | Senders.Add(queueOrTopicName, fakeSender); 27 | } 28 | return fakeSender; 29 | } 30 | 31 | public override ServiceBusProcessor CreateProcessor(string queueName, ServiceBusProcessorOptions options) 32 | { 33 | if (!Processors.TryGetValue(queueName, out var fakeProcessor)) 34 | { 35 | fakeProcessor = new FakeProcessor(); 36 | Processors.Add(queueName, fakeProcessor); 37 | } 38 | return fakeProcessor; 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/Tests/NServiceBus.Transport.AzureServiceBus.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.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/Tests/RecordingServiceBusAdministrationClient.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.Tests; 2 | 3 | using System.Text; 4 | using System.Text.Json; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Azure; 8 | using Azure.Messaging.ServiceBus.Administration; 9 | 10 | public class RecordingServiceBusAdministrationClient(StringBuilder builder = null) : ServiceBusAdministrationClient 11 | { 12 | readonly StringBuilder builder = builder ?? new StringBuilder(); 13 | 14 | public override Task> CreateSubscriptionAsync(CreateSubscriptionOptions options, CreateRuleOptions rule, 15 | CancellationToken cancellationToken = default) 16 | { 17 | builder.AppendLine($"CreateSubscriptionOptions: {JsonSerializer.Serialize(options, SkdJsonSerializerContext.PolymorphicOptions)}"); 18 | builder.AppendLine($"CreateRuleOptions: {JsonSerializer.Serialize(rule, SkdJsonSerializerContext.PolymorphicOptions)}"); 19 | return Task.FromResult>(null); 20 | } 21 | 22 | public override Task> CreateTopicAsync(CreateTopicOptions options, CancellationToken cancellationToken = default) 23 | { 24 | builder.AppendLine($"CreateTopicOptions: {JsonSerializer.Serialize(options, SkdJsonSerializerContext.PolymorphicOptions)}"); 25 | return Task.FromResult>(null); 26 | } 27 | 28 | public override Task> CreateRuleAsync(string topicName, string subscriptionName, CreateRuleOptions options, 29 | CancellationToken cancellationToken = default) 30 | { 31 | builder.AppendLine($"CreateRuleOptions(topicName: '{topicName}', subscriptionName: '{subscriptionName}'): {JsonSerializer.Serialize(options, SkdJsonSerializerContext.PolymorphicOptions)}"); 32 | return Task.FromResult>(null); 33 | } 34 | 35 | public override string ToString() => builder.ToString(); 36 | } -------------------------------------------------------------------------------- /src/Tests/RecordingServiceBusClient.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.Tests; 2 | 3 | using System.Text; 4 | using System.Text.Json; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Azure.Messaging.ServiceBus; 8 | using Azure.Messaging.ServiceBus.Administration; 9 | 10 | public class RecordingServiceBusClient(StringBuilder builder = null) : ServiceBusClient 11 | { 12 | readonly StringBuilder builder = builder ?? new StringBuilder(); 13 | 14 | public override ServiceBusRuleManager CreateRuleManager(string topicName, string subscriptionName) => new RecordingRuleManager(topicName, subscriptionName, builder); 15 | 16 | public override string ToString() => builder.ToString(); 17 | 18 | class RecordingRuleManager(string topicName, string subscriptionName, StringBuilder builder) : ServiceBusRuleManager 19 | { 20 | public override Task CreateRuleAsync(CreateRuleOptions options, CancellationToken cancellationToken = default) 21 | { 22 | builder.AppendLine($"CreateRuleOptions(topicName: '{topicName}', subscriptionName: '{subscriptionName}'): {JsonSerializer.Serialize(options, SkdJsonSerializerContext.PolymorphicOptions)}"); 23 | return Task.CompletedTask; 24 | } 25 | 26 | public override Task CreateRuleAsync(string ruleName, RuleFilter filter, CancellationToken cancellationToken = default) 27 | { 28 | builder.AppendLine($"RuleFilter(topicName: '{topicName}', subscriptionName: '{subscriptionName}', , ruleName: '{ruleName}'): {JsonSerializer.Serialize(filter, SkdJsonSerializerContext.PolymorphicOptions)}"); 29 | return Task.CompletedTask; 30 | } 31 | 32 | public override Task CloseAsync(CancellationToken cancellationToken = default) => Task.CompletedTask; 33 | } 34 | } -------------------------------------------------------------------------------- /src/Tests/Sending/MessageRegistryTests.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.Tests.Sending 2 | { 3 | using System.Threading.Tasks; 4 | using Azure.Messaging.ServiceBus; 5 | using NUnit.Framework; 6 | 7 | [TestFixture] 8 | public class MessageRegistryTests 9 | { 10 | [Test] 11 | public async Task Should_get_cached_sender_per_destination() 12 | { 13 | var pool = new MessageSenderRegistry(new ServiceBusClient(FakeConnectionString)); 14 | 15 | try 16 | { 17 | var firstMessageSenderDest1 = pool.GetMessageSender("dest1", null); 18 | 19 | var firstMessageSenderDest2 = pool.GetMessageSender("dest2", null); 20 | 21 | var secondMessageSenderDest1 = pool.GetMessageSender("dest1", null); 22 | var secondMessageSenderDest2 = pool.GetMessageSender("dest2", null); 23 | 24 | Assert.Multiple(() => 25 | { 26 | Assert.That(secondMessageSenderDest1, Is.SameAs(firstMessageSenderDest1)); 27 | Assert.That(secondMessageSenderDest2, Is.SameAs(firstMessageSenderDest2)); 28 | Assert.That(firstMessageSenderDest2, Is.Not.SameAs(firstMessageSenderDest1)); 29 | }); 30 | Assert.That(secondMessageSenderDest2, Is.Not.SameAs(secondMessageSenderDest1)); 31 | } 32 | finally 33 | { 34 | await pool.Close(); 35 | } 36 | } 37 | 38 | static readonly string FakeConnectionString = "Endpoint=sb://fake.servicebus.windows.net/;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=fake="; 39 | } 40 | } -------------------------------------------------------------------------------- /src/Tests/Sending/OutgoingMessageExtensionsFixture.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.Tests.Sending 2 | { 3 | using System; 4 | using Azure.Messaging.ServiceBus; 5 | using NServiceBus.Routing; 6 | using NUnit.Framework; 7 | 8 | [TestFixture] 9 | public class OutgoingMessageExtensionsTests 10 | { 11 | const string TransportEncoding = "NServiceBus.Transport.Encoding"; 12 | const string Dummy = "DUMMY"; 13 | 14 | [Test] 15 | public void Should_not_contain_legacy_header_by_default() 16 | { 17 | // Arrange 18 | TransportOperation transportOperation = CreateTransportOperation(); 19 | var transportOperations = new TransportOperations(transportOperation); 20 | var outgoingMessage = transportOperations.UnicastTransportOperations[0]; 21 | 22 | // Act 23 | ServiceBusMessage serviceBusMessage = outgoingMessage.ToAzureServiceBusMessage(Dummy, false); 24 | 25 | Assert.That(serviceBusMessage.ApplicationProperties.Keys, Has.No.Member(TransportEncoding)); 26 | } 27 | 28 | [Test] 29 | public void Should_contain_legacy_header_when_opted_in() 30 | { 31 | // Arrange 32 | TransportOperation transportOperation = CreateTransportOperation(); 33 | var transportOperations = new TransportOperations(transportOperation); 34 | var outgoingMessage = transportOperations.UnicastTransportOperations[0]; 35 | 36 | // Act 37 | ServiceBusMessage serviceBusMessage = outgoingMessage.ToAzureServiceBusMessage(Dummy, true); 38 | 39 | Assert.That(serviceBusMessage.ApplicationProperties.Keys, Has.Member(TransportEncoding)); 40 | } 41 | 42 | static TransportOperation CreateTransportOperation() 43 | { 44 | var messageId = Guid.NewGuid().ToString(); 45 | var message = new OutgoingMessage(messageId, [], ReadOnlyMemory.Empty); 46 | var transportOperation = new TransportOperation( 47 | message, 48 | addressTag: new UnicastAddressTag(Dummy), 49 | properties: null, 50 | DispatchConsistency.Default 51 | ); 52 | return transportOperation; 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/Tests/SkdJsonSerializerContext.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.Tests; 2 | 3 | using System.Text.Json; 4 | using System.Text.Json.Serialization; 5 | using System.Text.Json.Serialization.Metadata; 6 | using Azure.Messaging.ServiceBus.Administration; 7 | 8 | [JsonSourceGenerationOptions(WriteIndented = true)] 9 | [JsonSerializable(typeof(CreateSubscriptionOptions))] 10 | [JsonSerializable(typeof(CreateTopicOptions))] 11 | [JsonSerializable(typeof(CreateRuleOptions))] 12 | public partial class SkdJsonSerializerContext : JsonSerializerContext 13 | { 14 | static JsonSerializerOptions options; 15 | 16 | public static JsonSerializerOptions PolymorphicOptions => 17 | options ??= new JsonSerializerOptions(Default.Options) 18 | { 19 | TypeInfoResolver = new DefaultJsonTypeInfoResolver 20 | { 21 | Modifiers = 22 | { 23 | typeInfo => 24 | { 25 | if (typeInfo.Type == typeof(RuleFilter)) 26 | { 27 | typeInfo.PolymorphismOptions = new JsonPolymorphismOptions 28 | { 29 | TypeDiscriminatorPropertyName = "filter-type", 30 | UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization, 31 | DerivedTypes = 32 | { 33 | new JsonDerivedType(typeof(CorrelationRuleFilter), "correlation"), 34 | new JsonDerivedType(typeof(SqlRuleFilter), "sql"), 35 | new JsonDerivedType(typeof(TrueRuleFilter), "true"), 36 | new JsonDerivedType(typeof(FalseRuleFilter), "false") 37 | } 38 | }; 39 | } 40 | } 41 | } 42 | } 43 | }; 44 | } -------------------------------------------------------------------------------- /src/Tests/Testing/TestableCustomizeNativeMessageExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.Tests.Testing 2 | { 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Azure.Messaging.ServiceBus; 6 | using NServiceBus.Testing; 7 | using NUnit.Framework; 8 | 9 | [TestFixture] 10 | public class TestableCustomizeNativeMessageExtensionsTests 11 | { 12 | [Test] 13 | public async Task GetNativeMessageCustomization_should_return_customization() 14 | { 15 | var testableContext = new TestableMessageHandlerContext(); 16 | 17 | var handler = new MyHandlerUsingCustomizations(); 18 | 19 | await handler.Handle(new MyMessage(), testableContext); 20 | 21 | var publishedMessage = testableContext.PublishedMessages.Single(); 22 | var customization = publishedMessage.Options.GetNativeMessageCustomization(); 23 | 24 | var nativeMessage = new ServiceBusMessage(); 25 | customization(nativeMessage); 26 | 27 | Assert.That(nativeMessage.Subject, Is.EqualTo("abc")); 28 | } 29 | 30 | [Test] 31 | public async Task GetNativeMessageCustomization_when_no_customization_should_return_null() 32 | { 33 | var testableContext = new TestableMessageHandlerContext(); 34 | 35 | var handler = new MyHandlerWithoutCustomization(); 36 | 37 | await handler.Handle(new MyMessage(), testableContext); 38 | 39 | var publishedMessage = testableContext.PublishedMessages.Single(); 40 | var customization = publishedMessage.Options.GetNativeMessageCustomization(); 41 | 42 | Assert.That(customization, Is.Null); 43 | } 44 | 45 | class MyHandlerUsingCustomizations : IHandleMessages 46 | { 47 | public async Task Handle(MyMessage message, IMessageHandlerContext context) 48 | { 49 | var options = new PublishOptions(); 50 | options.CustomizeNativeMessage(m => m.Subject = "abc"); 51 | await context.Publish(message, options); 52 | } 53 | } 54 | 55 | class MyHandlerWithoutCustomization : IHandleMessages 56 | { 57 | public async Task Handle(MyMessage message, IMessageHandlerContext context) 58 | { 59 | var options = new PublishOptions(); 60 | await context.Publish(message, options); 61 | } 62 | } 63 | 64 | class MyMessage 65 | { 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/Transport/Administration/NamespacePermissions.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus 2 | { 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Azure.Messaging.ServiceBus.Administration; 7 | 8 | static class NamespacePermissions 9 | { 10 | public static async Task AssertNamespaceManageRightsAvailable(this ServiceBusAdministrationClient administrationClient, CancellationToken cancellationToken = default) 11 | { 12 | try 13 | { 14 | await administrationClient.QueueExistsAsync("$nservicebus-verification-queue", cancellationToken) 15 | .ConfigureAwait(false); 16 | } 17 | catch (UnauthorizedAccessException e) 18 | { 19 | throw new Exception("Management rights are required to run this endpoint. Verify that the SAS policy has the Manage claim.", e); 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Transport/Administration/QueueCreator.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Azure.Messaging.ServiceBus; 7 | using Azure.Messaging.ServiceBus.Administration; 8 | using Logging; 9 | 10 | class QueueCreator(AzureServiceBusTransport transportSettings) 11 | { 12 | static readonly ILog Logger = LogManager.GetLogger(); 13 | 14 | public async Task Create(ServiceBusAdministrationClient adminClient, string[] queues, 15 | CancellationToken cancellationToken = default) 16 | { 17 | foreach (var address in queues) 18 | { 19 | var queue = new CreateQueueOptions(address) 20 | { 21 | EnableBatchedOperations = true, 22 | LockDuration = TimeSpan.FromMinutes(5), 23 | MaxDeliveryCount = int.MaxValue, 24 | MaxSizeInMegabytes = transportSettings.EntityMaximumSizeInMegabytes, 25 | EnablePartitioning = transportSettings.EnablePartitioning 26 | }; 27 | 28 | try 29 | { 30 | await adminClient.CreateQueueAsync(queue, cancellationToken).ConfigureAwait(false); 31 | } 32 | catch (ServiceBusException sbe) when (sbe.Reason == ServiceBusFailureReason.MessagingEntityAlreadyExists) 33 | { 34 | if (Logger.IsDebugEnabled) 35 | { 36 | Logger.Debug($"Queue {queue.Name} already exists"); 37 | } 38 | } 39 | catch (ServiceBusException sbe) when (sbe.IsTransient)// An operation is in progress. 40 | { 41 | Logger.Info($"Queue creation for {queue.Name} is already in progress"); 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /src/Transport/Administration/TopologyCreator.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Azure.Messaging.ServiceBus.Administration; 6 | 7 | class TopologyCreator(AzureServiceBusTransport transportSettings) 8 | { 9 | public async Task Create(ServiceBusAdministrationClient adminClient, string[] queues, 10 | CancellationToken cancellationToken = default) 11 | { 12 | var topologyCreator = new MigrationTopologyCreator(transportSettings); 13 | await topologyCreator.Create(adminClient, cancellationToken).ConfigureAwait(false); 14 | 15 | var queueCreator = new QueueCreator(transportSettings); 16 | await queueCreator.Create(adminClient, queues, cancellationToken).ConfigureAwait(false); 17 | } 18 | } -------------------------------------------------------------------------------- /src/Transport/AzureServiceBusTransportTransaction.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using System; 4 | using System.Transactions; 5 | using Azure.Messaging.ServiceBus; 6 | 7 | /// 8 | /// The Azure Service Bus transaction encapsulates the logic of sharing and accessing the connection information 9 | /// required to support cross entity transaction ( set to true) 10 | /// such as the , the and the 11 | /// between the incoming message (if available) and the outgoing messages dispatched. 12 | /// 13 | /// This class is not thread safe. 14 | public sealed class AzureServiceBusTransportTransaction : IDisposable 15 | { 16 | /// 17 | /// Creates a new instance of an with an optional transport transaction. 18 | /// 19 | /// The current instance registers itself automatically in the transport transaction. 20 | /// An optional transport transaction instance. 21 | public AzureServiceBusTransportTransaction(TransportTransaction? transportTransaction = null) 22 | { 23 | TransportTransaction = transportTransaction ?? new TransportTransaction(); 24 | TransportTransaction.Set(this); 25 | } 26 | 27 | /// 28 | /// Creates a new instance of an with the provided connection information. 29 | /// The connection information is necessary in cases cross entity transactions are in use. 30 | /// 31 | /// The current instance registers itself automatically in the transport transaction. 32 | /// The service bus client to be used for creating senders. 33 | /// The incoming queue partition key to be used to set 34 | /// The transaction options to be used when the underlying committable transaction is created. 35 | /// An optional transport transaction instance. 36 | public AzureServiceBusTransportTransaction(ServiceBusClient serviceBusClient, string incomingQueuePartitionKey, 37 | TransactionOptions transactionOptions, TransportTransaction? transportTransaction = null) 38 | : this(transportTransaction) 39 | { 40 | ServiceBusClient = serviceBusClient; 41 | IncomingQueuePartitionKey = incomingQueuePartitionKey; 42 | this.transactionOptions = transactionOptions; 43 | } 44 | 45 | /// 46 | /// Gets the currently owned transport transaction. 47 | /// 48 | public TransportTransaction TransportTransaction { get; } 49 | 50 | /// 51 | /// Gets the in case cross entity transactions 52 | /// are enabled. 53 | /// 54 | /// A transaction or null. 55 | /// The transaction is lazy initialized as late as possible to make sure 56 | /// the transaction timeout is only started when the transaction is really needed. 57 | internal Transaction? Transaction 58 | { 59 | get 60 | { 61 | if (transactionIsInitialized) 62 | { 63 | return transaction; 64 | } 65 | 66 | transaction = transactionOptions.HasValue ? new CommittableTransaction(transactionOptions.Value) : default; 67 | transactionIsInitialized = true; 68 | return transaction; 69 | } 70 | } 71 | 72 | internal bool HasTransaction => transaction != null || transactionOptions.HasValue; 73 | 74 | internal ServiceBusClient? ServiceBusClient 75 | { 76 | get; 77 | } 78 | 79 | internal string? IncomingQueuePartitionKey 80 | { 81 | get; 82 | } 83 | 84 | /// 85 | /// Commits the underlying committable transaction in case one was created. 86 | /// 87 | public void Commit() => transaction?.Commit(); 88 | 89 | /// 90 | /// Disposes the underlying committable transaction in case one was created. 91 | /// 92 | public void Dispose() => transaction?.Dispose(); 93 | 94 | readonly TransactionOptions? transactionOptions; 95 | CommittableTransaction? transaction; 96 | bool transactionIsInitialized; 97 | } -------------------------------------------------------------------------------- /src/Transport/AzureServiceBusTransportTransactionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using System.Transactions; 4 | 5 | /// 6 | /// Provides extension methods for 7 | /// 8 | public static class AzureServiceBusTransportTransactionExtensions 9 | { 10 | /// 11 | /// Returns a new scope that takes into account the internally managed transaction by either 12 | /// providing the transaction to the scope or creating a suppress scope. 13 | /// 14 | public static TransactionScope ToTransactionScope( 15 | this AzureServiceBusTransportTransaction azureServiceBusTransaction) => 16 | azureServiceBusTransaction.Transaction.ToScope(); 17 | } -------------------------------------------------------------------------------- /src/Transport/Configuration/DiagnosticDescriptors.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.Configuration; 2 | 3 | static class DiagnosticDescriptors 4 | { 5 | // https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/choosing-diagnostic-ids 6 | public const string ExperimentalQueuesAttribute = "NSBASBEXP0001"; 7 | public const string ExperimentalRulesAttribute = "NSBASBEXP0002"; 8 | public const string ExperimentalSubscriptionsAttribute = "NSBASBEXP0003"; 9 | public const string ExperimentalTopicsAttribute = "NSBASBEXP0004"; 10 | public const string ExperimentalValidMigrationTopologyAttribute = "NSBASBEXP0005"; 11 | } -------------------------------------------------------------------------------- /src/Transport/Configuration/TransportMessageHeaders.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.Configuration; 2 | 3 | static class TransportMessageHeaders 4 | { 5 | public const string TransportEncoding = "NServiceBus.Transport.Encoding"; 6 | } -------------------------------------------------------------------------------- /src/Transport/EventRouting/AzureServiceBusQueuesAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.DataAnnotations; 6 | using System.Diagnostics.CodeAnalysis; 7 | using Configuration; 8 | 9 | /// 10 | /// Validates whether the values in a dictionary passed to the property are valid Azure Service Bus queues. 11 | /// 12 | [Experimental(DiagnosticDescriptors.ExperimentalQueuesAttribute)] 13 | [AttributeUsage(AttributeTargets.Property)] 14 | public sealed class AzureServiceBusQueuesAttribute : ValidationAttribute 15 | { 16 | /// 17 | protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => 18 | value switch 19 | { 20 | Dictionary dic => EntityValidator.ValidateQueues(dic.Keys, validationContext.MemberName), 21 | _ => ValidationResult.Success, 22 | }; 23 | } -------------------------------------------------------------------------------- /src/Transport/EventRouting/AzureServiceBusRulesAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.DataAnnotations; 6 | using System.Diagnostics.CodeAnalysis; 7 | using Configuration; 8 | 9 | /// 10 | /// Validates whether the values in a dictionary passed to the property are valid Azure Service Bus rules. 11 | /// 12 | [Experimental(DiagnosticDescriptors.ExperimentalRulesAttribute)] 13 | [AttributeUsage(AttributeTargets.Property)] 14 | public sealed class AzureServiceBusRulesAttribute : ValidationAttribute 15 | { 16 | /// 17 | protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => 18 | value switch 19 | { 20 | Dictionary dic => EntityValidator.ValidateRules(dic.Values, validationContext.MemberName), 21 | _ => ValidationResult.Success, 22 | }; 23 | } -------------------------------------------------------------------------------- /src/Transport/EventRouting/AzureServiceBusSubscriptionsAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.DataAnnotations; 6 | using System.Diagnostics.CodeAnalysis; 7 | using Configuration; 8 | 9 | /// 10 | /// Validates whether the values in a dictionary passed to the property are valid Azure Service Bus subscriptions. 11 | /// 12 | [Experimental(DiagnosticDescriptors.ExperimentalSubscriptionsAttribute)] 13 | [AttributeUsage(AttributeTargets.Property)] 14 | public sealed class AzureServiceBusSubscriptionsAttribute : ValidationAttribute 15 | { 16 | /// 17 | protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => 18 | value switch 19 | { 20 | Dictionary dic => EntityValidator.ValidateSubscriptions(dic.Values, validationContext.MemberName), 21 | _ => ValidationResult.Success, 22 | }; 23 | } -------------------------------------------------------------------------------- /src/Transport/EventRouting/AzureServiceBusTopicsAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.DataAnnotations; 6 | using System.Diagnostics.CodeAnalysis; 7 | using System.Linq; 8 | using Configuration; 9 | 10 | /// 11 | /// Validates whether the string value, the values in a dictionary or the values in a dictionary of hashsets 12 | /// passed to the property are valid Azure Service Bus topics. 13 | /// 14 | [Experimental(DiagnosticDescriptors.ExperimentalTopicsAttribute)] 15 | [AttributeUsage(AttributeTargets.Property)] 16 | public sealed class AzureServiceBusTopicsAttribute : ValidationAttribute 17 | { 18 | /// 19 | protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => 20 | value switch 21 | { 22 | string topic => EntityValidator.ValidateTopics([topic], validationContext.MemberName), 23 | Dictionary dic => EntityValidator.ValidateTopics(dic.Values, validationContext.MemberName), 24 | Dictionary> set => EntityValidator.ValidateTopics(set.Values.SelectMany(x => x), 25 | validationContext.MemberName), 26 | _ => ValidationResult.Success, 27 | }; 28 | } -------------------------------------------------------------------------------- /src/Transport/EventRouting/EntityValidator.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | using System.Linq; 6 | using System.Text.RegularExpressions; 7 | 8 | static partial class EntityValidator 9 | { 10 | public static ValidationResult? ValidateTopics(IEnumerable topicNames, string? memberName) 11 | { 12 | var topicNameRegex = TopicNameRegex(); 13 | var invalidTopics = topicNames.Where(t => !topicNameRegex.IsMatch(t)).ToArray(); 14 | 15 | return invalidTopics.Any() 16 | ? new ValidationResult( 17 | $"The following topic name(s) do not comply with the Azure Service Bus topic limits: {string.Join(", ", invalidTopics)}", 18 | memberName is not null ? [memberName] : []) 19 | : ValidationResult.Success; 20 | } 21 | 22 | [GeneratedRegex(@"^(?=.{1,260}$)(?=^[A-Za-z0-9])(?!.*[\\?#])(?:[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9./_-]*[A-Za-z0-9])$")] 23 | private static partial Regex TopicNameRegex(); 24 | 25 | public static ValidationResult? ValidateQueues(IEnumerable queueNames, string? memberName) 26 | { 27 | var queueNameRegex = QueueNameRegex(); 28 | var invalidQueues = queueNames.Where(t => !queueNameRegex.IsMatch(t)).ToArray(); 29 | 30 | return invalidQueues.Any() 31 | ? new ValidationResult( 32 | $"The following queue name(s) do not comply with the Azure Service Bus queue limits: {string.Join(", ", invalidQueues)}", 33 | memberName is not null ? [memberName] : []) 34 | : ValidationResult.Success; 35 | } 36 | 37 | // Note the queue pattern is the same as the topic pattern. Deliberately kept separate for future extensibility. 38 | [GeneratedRegex(@"^(?=.{1,260}$)(?=^[A-Za-z0-9])(?!.*[\\?#])(?:[A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9./_-]*[A-Za-z0-9])$")] 39 | private static partial Regex QueueNameRegex(); 40 | 41 | public static ValidationResult? ValidateRules(IEnumerable ruleNames, string? memberName) 42 | { 43 | var ruleNameRegex = RuleNameRegex(); 44 | var invalidRules = ruleNames.Where(t => !ruleNameRegex.IsMatch(t)).ToArray(); 45 | 46 | return invalidRules.Any() 47 | ? new ValidationResult( 48 | $"The following rule name(s) do not comply with the Azure Service Bus rule limits: {string.Join(", ", invalidRules)}", 49 | memberName is not null ? [memberName] : []) 50 | : ValidationResult.Success; 51 | } 52 | 53 | [GeneratedRegex(@"^(?!\$)(?=.{1,50}$)(?=^[A-Za-z0-9])(?!.*[\/\\?#])[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$")] 54 | private static partial Regex RuleNameRegex(); 55 | 56 | public static ValidationResult? ValidateSubscriptions(IEnumerable subscriptionNames, 57 | string? memberName) 58 | { 59 | var subscriptionNameRegex = SubscriptionNameRegex(); 60 | var invalidSubscriptions = subscriptionNames.Where(t => !subscriptionNameRegex.IsMatch(t)).ToArray(); 61 | 62 | return invalidSubscriptions.Any() 63 | ? new ValidationResult( 64 | $"The following subscription name(s) do not comply with the Azure Service Bus subscription limits: {string.Join(", ", invalidSubscriptions)}", 65 | memberName is not null ? [memberName] : []) 66 | : ValidationResult.Success; 67 | } 68 | 69 | // Note the subscription pattern is the same as the rule pattern. Deliberately kept separate for future extensibility. 70 | [GeneratedRegex(@"^(?!\$)(?=.{1,50}$)(?=^[A-Za-z0-9])(?!.*[\/\\?#])[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$")] 71 | private static partial Regex SubscriptionNameRegex(); 72 | } -------------------------------------------------------------------------------- /src/Transport/EventRouting/MigrationTopologyCreator.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using System; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Azure.Messaging.ServiceBus; 7 | using Azure.Messaging.ServiceBus.Administration; 8 | using Logging; 9 | 10 | sealed class MigrationTopologyCreator(AzureServiceBusTransport transportSettings) 11 | { 12 | static readonly ILog Logger = LogManager.GetLogger(); 13 | 14 | public async Task Create(ServiceBusAdministrationClient adminClient, CancellationToken cancellationToken = default) 15 | { 16 | if (transportSettings.Topology is MigrationTopology migrationTopology) 17 | { 18 | var topicToPublishTo = new CreateTopicOptions(migrationTopology.TopicToPublishTo) 19 | { 20 | EnableBatchedOperations = true, 21 | EnablePartitioning = transportSettings.EnablePartitioning, 22 | MaxSizeInMegabytes = transportSettings.EntityMaximumSizeInMegabytes 23 | }; 24 | 25 | try 26 | { 27 | await adminClient.CreateTopicAsync(topicToPublishTo, cancellationToken).ConfigureAwait(false); 28 | } 29 | catch (ServiceBusException sbe) when (sbe.Reason == ServiceBusFailureReason.MessagingEntityAlreadyExists) 30 | { 31 | Logger.Info($"Topic {topicToPublishTo.Name} already exists"); 32 | } 33 | catch (ServiceBusException sbe) when (sbe.IsTransient) // An operation is in progress. 34 | { 35 | Logger.Info($"Topic creation for {topicToPublishTo.Name} is already in progress"); 36 | } 37 | 38 | if (migrationTopology.IsHierarchy) 39 | { 40 | var topicToSubscribeOn = new CreateTopicOptions(migrationTopology.TopicToSubscribeOn) 41 | { 42 | EnableBatchedOperations = true, 43 | EnablePartitioning = transportSettings.EnablePartitioning, 44 | MaxSizeInMegabytes = transportSettings.EntityMaximumSizeInMegabytes, 45 | }; 46 | 47 | try 48 | { 49 | await adminClient.CreateTopicAsync(topicToSubscribeOn, cancellationToken).ConfigureAwait(false); 50 | } 51 | catch (ServiceBusException sbe) when 52 | (sbe.Reason == ServiceBusFailureReason.MessagingEntityAlreadyExists) 53 | { 54 | Logger.Info($"Topic {topicToSubscribeOn.Name} already exists"); 55 | } 56 | catch (ServiceBusException sbe) when (sbe.IsTransient) // An operation is in progress. 57 | { 58 | Logger.Info($"Topic creation for {topicToSubscribeOn.Name} is already in progress"); 59 | } 60 | 61 | var subscription = 62 | new CreateSubscriptionOptions(migrationTopology.TopicToPublishTo, 63 | $"forwardTo-{migrationTopology.TopicToSubscribeOn}") 64 | { 65 | LockDuration = TimeSpan.FromMinutes(5), 66 | ForwardTo = migrationTopology.TopicToSubscribeOn, 67 | EnableDeadLetteringOnFilterEvaluationExceptions = false, 68 | MaxDeliveryCount = int.MaxValue, 69 | EnableBatchedOperations = true, 70 | UserMetadata = migrationTopology.TopicToSubscribeOn 71 | }; 72 | 73 | try 74 | { 75 | await adminClient.CreateSubscriptionAsync(subscription, 76 | new CreateRuleOptions("$default", new TrueRuleFilter()), cancellationToken) 77 | .ConfigureAwait(false); 78 | } 79 | catch (ServiceBusException sbe) when 80 | (sbe.Reason == ServiceBusFailureReason.MessagingEntityAlreadyExists) 81 | { 82 | if (Logger.IsDebugEnabled) 83 | { 84 | Logger.Debug($"Default subscription rule for topic {subscription.TopicName} already exists"); 85 | } 86 | } 87 | catch (ServiceBusException sbe) when (sbe.IsTransient) // An operation is in progress. 88 | { 89 | Logger.Info($"Default subscription rule for topic {subscription.TopicName} is already in progress"); 90 | } 91 | } 92 | } 93 | } 94 | } -------------------------------------------------------------------------------- /src/Transport/EventRouting/MigrationTopologyOptions.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using System.Collections.Generic; 4 | using System.ComponentModel.DataAnnotations; 5 | 6 | /// 7 | /// Serializable object that defines the migration topology 8 | /// 9 | [ObsoleteEx(Message = MigrationTopology.ObsoleteMessage, TreatAsErrorFromVersion = "7", RemoveInVersion = "8")] 10 | public sealed class MigrationTopologyOptions : TopologyOptions 11 | { 12 | /// 13 | /// Gets the topic name of the topic where all single-topic events are published to. 14 | /// 15 | [Required] 16 | [AzureServiceBusTopics] 17 | public required string? TopicToPublishTo { get; init; } 18 | 19 | /// 20 | /// Gets the topic name of the topic where all single-topic subscriptions are managed on. 21 | /// 22 | [Required] 23 | [AzureServiceBusTopics] 24 | public required string? TopicToSubscribeOn { get; init; } 25 | 26 | /// 27 | /// Collection of events that have not yet been migrated to the topic-per-event topology 28 | /// 29 | [ValidMigrationTopology] 30 | public HashSet EventsToMigrateMap 31 | { 32 | get => eventsToMigrateMap; 33 | init => eventsToMigrateMap = value ?? []; 34 | } 35 | 36 | /// 37 | /// Maps event full names to non-default rule names. 38 | /// 39 | [AzureServiceBusRules] 40 | public Dictionary SubscribedEventToRuleNameMap 41 | { 42 | get => subscribedEventToRuleNameMap; 43 | init => subscribedEventToRuleNameMap = value ?? []; 44 | } 45 | 46 | //Backing fields are required because the Json serializes initializes properties to null if corresponding json element is missing 47 | readonly HashSet eventsToMigrateMap = []; 48 | readonly Dictionary subscribedEventToRuleNameMap = []; 49 | } -------------------------------------------------------------------------------- /src/Transport/EventRouting/MigrationTopologyOptionsValidator.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using Microsoft.Extensions.Options; 4 | 5 | /// 6 | /// Validates the . 7 | /// 8 | [ObsoleteEx(Message = MigrationTopology.ObsoleteMessage, TreatAsErrorFromVersion = "7", RemoveInVersion = "8")] 9 | [OptionsValidator] 10 | public partial class MigrationTopologyOptionsValidator : IValidateOptions; -------------------------------------------------------------------------------- /src/Transport/EventRouting/SubscribedEventToTopicsMapConverter.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Text.Json; 7 | using System.Text.Json.Serialization; 8 | 9 | sealed class SubscribedEventToTopicsMapConverter : JsonConverter>> 10 | { 11 | public override bool CanConvert(Type typeToConvert) => 12 | typeof(Dictionary>).IsAssignableFrom(typeToConvert); 13 | 14 | public override Dictionary> Read(ref Utf8JsonReader reader, Type typeToConvert, 15 | JsonSerializerOptions options) 16 | { 17 | if (reader.TokenType != JsonTokenType.StartObject) 18 | { 19 | throw new JsonException("Expected StartObject token"); 20 | } 21 | 22 | var map = new Dictionary>(); 23 | 24 | while (reader.Read()) 25 | { 26 | if (reader.TokenType == JsonTokenType.EndObject) 27 | { 28 | break; 29 | } 30 | 31 | if (reader.TokenType != JsonTokenType.PropertyName) 32 | { 33 | continue; 34 | } 35 | 36 | string key = reader.GetString() ?? throw new JsonException("Key cannot be null"); 37 | _ = reader.Read(); 38 | 39 | if (reader.TokenType == JsonTokenType.String) 40 | { 41 | string value = reader.GetString() ?? throw new JsonException("Value cannot be null"); 42 | map[key] = [value]; 43 | } 44 | else if (reader.TokenType == JsonTokenType.StartArray) 45 | { 46 | var set = new HashSet(); 47 | while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) 48 | { 49 | if (reader.TokenType == JsonTokenType.String) 50 | { 51 | _ = set.Add(reader.GetString() ?? throw new JsonException("Value cannot be null")); 52 | } 53 | else 54 | { 55 | throw new JsonException("Expected String token"); 56 | } 57 | } 58 | 59 | map[key] = set; 60 | } 61 | else 62 | { 63 | throw new JsonException("Expected String or StartArray token"); 64 | } 65 | } 66 | 67 | return map; 68 | } 69 | 70 | public override void Write(Utf8JsonWriter writer, Dictionary> value, 71 | JsonSerializerOptions options) 72 | { 73 | if (value == null) 74 | { 75 | writer.WriteNullValue(); 76 | return; 77 | } 78 | 79 | writer.WriteStartObject(); 80 | 81 | foreach (KeyValuePair> pair in value) 82 | { 83 | writer.WritePropertyName(pair.Key); 84 | 85 | if (pair.Value == null) 86 | { 87 | throw new JsonException("Value cannot be null"); 88 | } 89 | 90 | if (pair.Value.Count == 1) 91 | { 92 | writer.WriteStringValue(pair.Value.ElementAt(0)); 93 | } 94 | else 95 | { 96 | writer.WriteStartArray(); 97 | foreach (string item in pair.Value) 98 | { 99 | writer.WriteStringValue(item); 100 | } 101 | 102 | writer.WriteEndArray(); 103 | } 104 | } 105 | 106 | writer.WriteEndObject(); 107 | } 108 | } -------------------------------------------------------------------------------- /src/Transport/EventRouting/SubscriptionManager.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Extensibility; 6 | using Unicast.Messages; 7 | 8 | abstract class SubscriptionManager( 9 | SubscriptionManagerCreationOptions creationOptions) 10 | : ISubscriptionManager 11 | { 12 | protected SubscriptionManagerCreationOptions CreationOptions { get; } = creationOptions; 13 | 14 | public abstract Task SubscribeAll(MessageMetadata[] eventTypes, ContextBag context, 15 | CancellationToken cancellationToken = default); 16 | 17 | public abstract Task Unsubscribe(MessageMetadata eventType, ContextBag context, 18 | CancellationToken cancellationToken = default); 19 | 20 | public ValueTask SetupInfrastructureIfNecessary(CancellationToken cancellationToken = default) => 21 | CreationOptions.SetupInfrastructure ? SetupInfrastructureCore(cancellationToken) : default; 22 | 23 | protected virtual ValueTask SetupInfrastructureCore(CancellationToken cancellationToken = default) => default; 24 | } -------------------------------------------------------------------------------- /src/Transport/EventRouting/SubscriptionManagerCreationOptions.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using Azure.Messaging.ServiceBus; 4 | using Azure.Messaging.ServiceBus.Administration; 5 | 6 | sealed class SubscriptionManagerCreationOptions 7 | { 8 | public required string SubscribingQueueName { get; init; } 9 | 10 | public required ServiceBusAdministrationClient AdministrationClient { get; init; } 11 | 12 | public required ServiceBusClient Client { get; init; } 13 | 14 | public bool EnablePartitioning { get; init; } 15 | 16 | public int EntityMaximumSizeInMegabytes { get; init; } 17 | 18 | public bool SetupInfrastructure { get; init; } 19 | } -------------------------------------------------------------------------------- /src/Transport/EventRouting/TopologyOptions.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using System.Collections.Generic; 4 | using System.Text.Json.Serialization; 5 | 6 | /// 7 | /// Serializable object that defines the topic-per-event topology 8 | /// 9 | [JsonDerivedType(typeof(TopologyOptions), typeDiscriminator: "topology-options")] 10 | [JsonDerivedType(typeof(MigrationTopologyOptions), typeDiscriminator: "migration-topology-options")] 11 | public class TopologyOptions 12 | { 13 | /// 14 | /// Maps event type full names to topics under which they are to be published. 15 | /// 16 | [AzureServiceBusTopics] 17 | public Dictionary PublishedEventToTopicsMap 18 | { 19 | get => publishedEventToTopicsMap; 20 | init => publishedEventToTopicsMap = value ?? []; 21 | } 22 | 23 | /// 24 | /// Maps event type full names to topics under which they are to be subscribed. 25 | /// 26 | [AzureServiceBusTopics] 27 | [JsonConverter(typeof(SubscribedEventToTopicsMapConverter))] 28 | public Dictionary> SubscribedEventToTopicsMap 29 | { 30 | get => subscribedEventToTopicsMap; 31 | init => subscribedEventToTopicsMap = value ?? []; 32 | } 33 | 34 | /// 35 | /// Maps queue names to non-default subscription names. 36 | /// 37 | [AzureServiceBusQueues] 38 | [AzureServiceBusSubscriptions] 39 | public Dictionary QueueNameToSubscriptionNameMap 40 | { 41 | get => queueNameToSubscriptionNameMap; 42 | init => queueNameToSubscriptionNameMap = value ?? []; 43 | } 44 | 45 | //Backing fields are required because the Json serializes initializes properties to null if corresponding json element is missing 46 | readonly Dictionary publishedEventToTopicsMap = []; 47 | readonly Dictionary> subscribedEventToTopicsMap = []; 48 | readonly Dictionary queueNameToSubscriptionNameMap = []; 49 | } -------------------------------------------------------------------------------- /src/Transport/EventRouting/TopologyOptionsDisableValidationValidator.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using Microsoft.Extensions.Options; 4 | 5 | /// 6 | /// Does not validate the provided . 7 | /// 8 | public sealed class TopologyOptionsDisableValidationValidator : IValidateOptions 9 | { 10 | /// 11 | public ValidateOptionsResult Validate(string? name, TopologyOptions options) => ValidateOptionsResult.Success; 12 | } -------------------------------------------------------------------------------- /src/Transport/EventRouting/TopologyOptionsSerializationContext.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using System.Text.Json.Serialization; 4 | 5 | /// 6 | /// Allows loading the topology information from a json document. 7 | /// 8 | [JsonSourceGenerationOptions(WriteIndented = true)] 9 | [JsonSerializable(typeof(MigrationTopologyOptions))] 10 | [JsonSerializable(typeof(TopologyOptions))] 11 | public partial class TopologyOptionsSerializationContext : JsonSerializerContext; -------------------------------------------------------------------------------- /src/Transport/EventRouting/TopologyOptionsValidator.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using Microsoft.Extensions.Options; 4 | 5 | /// 6 | /// Validates the . 7 | /// 8 | [OptionsValidator] 9 | public partial class TopologyOptionsValidator : IValidateOptions; -------------------------------------------------------------------------------- /src/Transport/EventRouting/ValidMigrationTopologyAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel.DataAnnotations; 6 | using System.Diagnostics.CodeAnalysis; 7 | using Configuration; 8 | using Microsoft.Extensions.Options; 9 | 10 | /// 11 | /// Validates whether the are valid and do not contain conflicting mapped event types. 12 | /// 13 | [Experimental(DiagnosticDescriptors.ExperimentalValidMigrationTopologyAttribute)] 14 | [AttributeUsage(AttributeTargets.Property)] 15 | public sealed class ValidMigrationTopologyAttribute : ValidationAttribute 16 | { 17 | /// 18 | protected override ValidationResult? IsValid(object? value, ValidationContext validationContext) => 19 | validationContext.ObjectInstance switch 20 | { 21 | MigrationTopologyOptions options => ValidateMigrationTopology(options), 22 | _ => ValidationResult.Success, 23 | }; 24 | 25 | static ValidationResult? ValidateMigrationTopology(MigrationTopologyOptions options) 26 | { 27 | var builder = new ValidateOptionsResultBuilder(); 28 | 29 | foreach ((string? eventTypeFullname, string? topic) in options.PublishedEventToTopicsMap) 30 | { 31 | if (options.EventsToMigrateMap.Contains(eventTypeFullname)) 32 | { 33 | builder.AddResult(new ValidationResult( 34 | $"Event '{eventTypeFullname}' is in the migration map and in the published event to topics map. An event type cannot be marked for migration and mapped to a topic at the same time.", 35 | [nameof(options.PublishedEventToTopicsMap)])); 36 | } 37 | 38 | if (topic.Equals(options.TopicToPublishTo)) 39 | { 40 | builder.AddResult(new ValidationResult( 41 | $"The topic to publish '{topic}' for '{eventTypeFullname}' cannot be the sames as the topic to publish to '{options.TopicToPublishTo}' for the migration topology.", 42 | [nameof(options.TopicToPublishTo), nameof(options.PublishedEventToTopicsMap)])); 43 | } 44 | } 45 | 46 | foreach ((string? eventTypeFullname, HashSet topics) in options.SubscribedEventToTopicsMap) 47 | { 48 | if (options.EventsToMigrateMap.Contains(eventTypeFullname)) 49 | { 50 | builder.AddResult(new ValidationResult( 51 | $"Event '{eventTypeFullname}' is in the migration map and in the subscribed event to topics map. An event type cannot be marked for migration and mapped to a topic at the same time.", 52 | [nameof(options.SubscribedEventToTopicsMap)])); 53 | } 54 | 55 | foreach (string topic in topics) 56 | { 57 | if (topic.Equals(options.TopicToSubscribeOn)) 58 | { 59 | builder.AddResult(new ValidationResult( 60 | $"The topic to subscribe '{topic}' for '{eventTypeFullname}' cannot be the sames as the topic to subscribe to '{options.TopicToSubscribeOn}' for the migration topology.", 61 | [nameof(options.TopicToSubscribeOn), nameof(options.SubscribedEventToTopicsMap)])); 62 | } 63 | } 64 | } 65 | 66 | var result = builder.Build(); 67 | return result.Succeeded ? ValidationResult.Success : new ValidationResult(result.FailureMessage, [nameof(MigrationTopologyOptions.EventsToMigrateMap)]); 68 | } 69 | } -------------------------------------------------------------------------------- /src/Transport/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 | } -------------------------------------------------------------------------------- /src/Transport/FodyWeavers.xml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Transport/NServiceBus.Transport.AzureServiceBus.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | true 6 | ..\NServiceBus.snk 7 | Azure Service Bus transport for NServiceBus 8 | NSBASBEXP0001;NSBASBEXP0002;NSBASBEXP0003;NSBASBEXP0004;NSBASBEXP0005; 9 | enable 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/Transport/OutgoingNativeMessageCustomizationAction.cs: -------------------------------------------------------------------------------- 1 | global using OutgoingNativeMessageCustomizationAction = System.Action; 2 | -------------------------------------------------------------------------------- /src/Transport/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(string contextUrl) : Attribute 10 | { 11 | public string ContextUrl { get; } = contextUrl; 12 | 13 | public string? ReplacementTypeOrMember { get; set; } 14 | 15 | public string? Message { get; set; } 16 | 17 | public string? Note { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /src/Transport/Receiving/MessageExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Runtime.Serialization; 6 | using System.Xml; 7 | using Azure.Messaging.ServiceBus; 8 | using Configuration; 9 | 10 | static class MessageExtensions 11 | { 12 | public static Dictionary GetNServiceBusHeaders(this ServiceBusReceivedMessage message) 13 | { 14 | var headers = new Dictionary(message.ApplicationProperties.Count); 15 | 16 | foreach (var kvp in message.ApplicationProperties) 17 | { 18 | headers[kvp.Key] = kvp.Value?.ToString(); 19 | } 20 | 21 | headers.Remove(TransportMessageHeaders.TransportEncoding); 22 | 23 | if (!string.IsNullOrWhiteSpace(message.ReplyTo)) 24 | { 25 | headers[Headers.ReplyToAddress] = message.ReplyTo; 26 | } 27 | 28 | if (!string.IsNullOrWhiteSpace(message.CorrelationId)) 29 | { 30 | headers[Headers.CorrelationId] = message.CorrelationId; 31 | } 32 | 33 | return headers; 34 | } 35 | 36 | public static string GetMessageId(this ServiceBusReceivedMessage message) 37 | { 38 | if (string.IsNullOrEmpty(message.MessageId)) 39 | { 40 | throw new Exception("Azure Service Bus MessageId is required, but was not found. Ensure to assign MessageId to all Service Bus messages."); 41 | } 42 | 43 | return message.MessageId; 44 | } 45 | 46 | public static BinaryData GetBody(this ServiceBusReceivedMessage message) 47 | { 48 | var body = message.Body ?? new BinaryData([]); 49 | var memory = body.ToMemory(); 50 | 51 | if (memory.IsEmpty || 52 | !message.ApplicationProperties.TryGetValue(TransportMessageHeaders.TransportEncoding, out var value) || 53 | !value.Equals("wcf/byte-array")) 54 | { 55 | return body; 56 | } 57 | 58 | using var reader = XmlDictionaryReader.CreateBinaryReader(body.ToStream(), XmlDictionaryReaderQuotas.Max); 59 | var bodyBytes = (byte[])Deserializer.ReadObject(reader)!; 60 | return new BinaryData(bodyBytes); 61 | } 62 | 63 | static readonly DataContractSerializer Deserializer = new(typeof(byte[])); 64 | } -------------------------------------------------------------------------------- /src/Transport/Receiving/QueueAddressQualifier.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | /// 4 | /// Queue address qualifiers 5 | /// 6 | public static class QueueAddressQualifier 7 | { 8 | /// 9 | /// Qualifier that identifies the Azure Service Bus native dead-letter subqueue 10 | /// 11 | public const string DeadLetterQueue = "$DeadLetterQueue"; 12 | } -------------------------------------------------------------------------------- /src/Transport/Sending/CustomizeNativeMessageExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus; 2 | 3 | using System; 4 | using Azure.Messaging.ServiceBus; 5 | using Extensibility; 6 | 7 | /// 8 | /// Allows the users to customize outgoing native messages. 9 | /// 10 | /// 11 | /// The behavior of this class is exposed via extension methods. 12 | /// 13 | public static class CustomizeNativeMessageExtensions 14 | { 15 | /// 16 | /// Allows customization of the outgoing native message sent using . 17 | /// 18 | /// Option being extended. 19 | /// Customization action. 20 | public static void CustomizeNativeMessage(this ExtendableOptions options, Action customization) 21 | { 22 | var extensions = options.GetExtensions(); 23 | if (extensions.TryGet>(NativeMessageCustomizationBehavior.CustomizationKey, out _)) 24 | { 25 | throw new InvalidOperationException("Native outgoing message has already been customized. Do not apply native outgoing message customization more than once per message."); 26 | } 27 | 28 | extensions.Set(NativeMessageCustomizationBehavior.CustomizationKey, customization); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Transport/Sending/MessageSenderRegistry.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 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 Azure.Messaging.ServiceBus; 9 | 10 | sealed class MessageSenderRegistry(ServiceBusClient defaultClient) 11 | { 12 | public ServiceBusSender GetMessageSender(string destination, ServiceBusClient? client) 13 | { 14 | // According to the client SDK guidelines we can safely use these client objects for concurrent asynchronous 15 | // operations and from multiple threads. 16 | // see https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-performance-improvements 17 | var lazySender = destinationToSenderMapping.GetOrAdd((destination, client ?? defaultClient), 18 | static arg => 19 | { 20 | (string innerDestination, ServiceBusClient innerClient) = arg; 21 | // Unfortunately Lazy closure allocates but this should be fine since the majority of the 22 | // execution path will fall into the get and not the add. 23 | return new Lazy(() => innerClient.CreateSender(innerDestination, new ServiceBusSenderOptions { Identifier = $"Sender-{innerDestination}-{Guid.NewGuid()}" }), LazyThreadSafetyMode.ExecutionAndPublication); 24 | }); 25 | return lazySender.Value; 26 | } 27 | 28 | public Task Close(CancellationToken cancellationToken = default) 29 | { 30 | static async Task CloseAndDispose(ServiceBusSender sender, CancellationToken cancellationToken) 31 | { 32 | await using (sender.ConfigureAwait(false)) 33 | { 34 | await sender.CloseAsync(cancellationToken).ConfigureAwait(false); 35 | } 36 | } 37 | 38 | var tasks = new List(destinationToSenderMapping.Keys.Count); 39 | foreach (var key in destinationToSenderMapping.Keys) 40 | { 41 | var queue = destinationToSenderMapping[key]; 42 | 43 | if (!queue.IsValueCreated) 44 | { 45 | continue; 46 | } 47 | 48 | 49 | tasks.Add(CloseAndDispose(queue.Value, cancellationToken)); 50 | } 51 | return Task.WhenAll(tasks); 52 | } 53 | 54 | readonly ConcurrentDictionary<(string destination, ServiceBusClient client), Lazy> destinationToSenderMapping = new(); 55 | } -------------------------------------------------------------------------------- /src/Transport/Sending/NativeMessageCustomizationBehavior.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus; 2 | 3 | using System; 4 | using System.Threading.Tasks; 5 | using Azure.Messaging.ServiceBus; 6 | using Pipeline; 7 | using Transport; 8 | 9 | sealed class NativeMessageCustomizationBehavior(bool isOutboxEnabled) : Behavior 10 | { 11 | internal const string CustomizationKey = "$ASB.CustomizationId"; 12 | 13 | public override Task Invoke(IRoutingContext context, Func next) 14 | { 15 | if (!context.Extensions.TryGet(CustomizationKey, out Action customization)) 16 | { 17 | return next(); 18 | } 19 | 20 | if (isOutboxEnabled) 21 | { 22 | throw new Exception("Native message customization cannot be used together with the Outbox as customizations are not persistent. Disable the outbox to use native message customization."); 23 | } 24 | 25 | // When part of an incoming message, the transport transaction is set by the transport. 26 | // Otherwise it will be created at this point which works because there is no batched dispatch for IMessageSession operations. 27 | var transportTransaction = context.Extensions.GetOrCreate(); 28 | 29 | // Use the TransportTransaction to store complex objects and pass them to the transport. The transaction might be shared by multiple send operations. 30 | var customizer = transportTransaction.GetOrCreate(); 31 | 32 | var customizationId = Guid.NewGuid().ToString(); 33 | customizer.Customizations.TryAdd(customizationId, customization); 34 | 35 | // Store the key to the customization in the dispatch properties that are passed to the transport 36 | context.Extensions.Get()[CustomizationKey] = customizationId; 37 | 38 | return next(); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Transport/Sending/NativeMessageCustomizationFeature.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus; 2 | 3 | using Features; 4 | 5 | sealed class NativeMessageCustomizationFeature : Feature 6 | { 7 | public NativeMessageCustomizationFeature() => EnableByDefault(); 8 | 9 | protected override void Setup(FeatureConfigurationContext context) 10 | { 11 | var isOutboxEnabled = context.Settings.IsFeatureEnabled(typeof(Features.Outbox)); 12 | context.Pipeline.Register(new NativeMessageCustomizationBehavior(isOutboxEnabled), "Passes native message customizations to the transport"); 13 | } 14 | } -------------------------------------------------------------------------------- /src/Transport/Sending/NativeMessageCustomizer.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus; 2 | 3 | using System; 4 | using System.Collections.Concurrent; 5 | using Azure.Messaging.ServiceBus; 6 | 7 | sealed class NativeMessageCustomizer 8 | { 9 | ConcurrentDictionary>? customizations; 10 | public ConcurrentDictionary> Customizations => customizations ??= new ConcurrentDictionary>(); 11 | } -------------------------------------------------------------------------------- /src/Transport/Sending/OutgoingMessageExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using System; 4 | using System.Collections.Generic; 5 | using Azure.Messaging.ServiceBus; 6 | using Configuration; 7 | 8 | static class OutgoingMessageExtensions 9 | { 10 | public static ServiceBusMessage ToAzureServiceBusMessage( 11 | this IOutgoingTransportOperation outgoingTransportOperation, 12 | string? incomingQueuePartitionKey, 13 | bool sendTransportEncodingHeader = false 14 | ) 15 | { 16 | var outgoingMessage = outgoingTransportOperation.Message; 17 | var message = new ServiceBusMessage(outgoingMessage.Body) 18 | { 19 | // Cannot re-use MessageId to be compatible with ASB transport that could have native de-dup enabled 20 | MessageId = Guid.NewGuid().ToString() 21 | }; 22 | 23 | if (sendTransportEncodingHeader) 24 | { 25 | // The value needs to be "application/octect-stream" and not "application/octet-stream" for interop with ASB transport 26 | message.ApplicationProperties[TransportMessageHeaders.TransportEncoding] = "application/octect-stream"; 27 | } 28 | 29 | message.TransactionPartitionKey = incomingQueuePartitionKey; 30 | 31 | ApplyDeliveryConstraints(message, outgoingTransportOperation.Properties); 32 | 33 | ApplyCorrelationId(message, outgoingMessage.Headers); 34 | 35 | ApplyContentType(message, outgoingMessage.Headers); 36 | 37 | SetReplyToAddress(message, outgoingMessage.Headers); 38 | 39 | CopyHeaders(message, outgoingMessage.Headers); 40 | 41 | return message; 42 | } 43 | 44 | static void ApplyDeliveryConstraints(ServiceBusMessage message, DispatchProperties dispatchProperties) 45 | { 46 | if (dispatchProperties.DoNotDeliverBefore != null) 47 | { 48 | message.ScheduledEnqueueTime = dispatchProperties.DoNotDeliverBefore.At; 49 | } 50 | else if (dispatchProperties.DelayDeliveryWith != null) 51 | { 52 | // Delaying with TimeSpan is currently not supported, see https://github.com/Azure/azure-service-bus-dotnet/issues/160 53 | message.ScheduledEnqueueTime = DateTimeOffset.UtcNow + dispatchProperties.DelayDeliveryWith.Delay; 54 | } 55 | 56 | if (dispatchProperties.DiscardIfNotReceivedBefore != null) 57 | { 58 | message.TimeToLive = dispatchProperties.DiscardIfNotReceivedBefore.MaxTime; 59 | } 60 | } 61 | 62 | static void ApplyCorrelationId(ServiceBusMessage message, Dictionary headers) 63 | { 64 | if (headers.TryGetValue(Headers.CorrelationId, out var correlationId)) 65 | { 66 | message.CorrelationId = correlationId; 67 | } 68 | } 69 | 70 | static void ApplyContentType(ServiceBusMessage message, Dictionary headers) 71 | { 72 | if (headers.TryGetValue(Headers.ContentType, out var contentType)) 73 | { 74 | message.ContentType = contentType; 75 | } 76 | } 77 | 78 | static void SetReplyToAddress(ServiceBusMessage message, Dictionary headers) 79 | { 80 | if (headers.TryGetValue(Headers.ReplyToAddress, out var replyToAddress)) 81 | { 82 | message.ReplyTo = replyToAddress; 83 | } 84 | } 85 | 86 | static void CopyHeaders(ServiceBusMessage outgoingMessage, Dictionary headers) 87 | { 88 | foreach (var header in headers) 89 | { 90 | outgoingMessage.ApplicationProperties[header.Key] = header.Value; 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /src/Transport/Sending/OutgoingTransportOperationExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using System; 4 | using Azure.Messaging.ServiceBus; 5 | using Logging; 6 | 7 | static class OutgoingTransportOperationExtensions 8 | { 9 | public static void ApplyCustomizationToOutgoingNativeMessage( 10 | this IOutgoingTransportOperation transportOperation, 11 | ServiceBusMessage message, TransportTransaction transportTransaction, ILog logger) 12 | { 13 | if (!transportOperation.Properties.TryGetValue(NativeMessageCustomizationBehavior.CustomizationKey, 14 | out var key)) 15 | { 16 | return; 17 | } 18 | 19 | var messageCustomizer = transportTransaction.Get(); 20 | if (!messageCustomizer.Customizations.TryGetValue(key, out var action)) 21 | { 22 | logger.Warn( 23 | $"Message {transportOperation.Message.MessageId} was configured with a native message customization but the customization was not found in {nameof(NativeMessageCustomizer)}"); 24 | return; 25 | } 26 | 27 | action(message); 28 | } 29 | 30 | public static string ExtractDestination(this IOutgoingTransportOperation outgoingTransportOperation, 31 | TopicTopology topology) 32 | { 33 | switch (outgoingTransportOperation) 34 | { 35 | case MulticastTransportOperation multicastTransportOperation: 36 | return topology.GetPublishDestination(multicastTransportOperation.MessageType); 37 | case UnicastTransportOperation unicastTransportOperation: 38 | var destination = unicastTransportOperation.Destination; 39 | 40 | // Workaround for reply-to address set by ASB transport 41 | var index = unicastTransportOperation.Destination.IndexOf('@'); 42 | 43 | if (index > 0) 44 | { 45 | destination = destination[..index]; 46 | } 47 | 48 | return destination; 49 | default: 50 | throw new ArgumentOutOfRangeException(nameof(outgoingTransportOperation)); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/Transport/Testing/TestableCustomizeNativeMessageExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Testing; 2 | 3 | using System; 4 | using Azure.Messaging.ServiceBus; 5 | using Extensibility; 6 | 7 | /// 8 | /// Provides helper implementations for the native message customization for testing purposes. 9 | /// 10 | public static class TestableCustomizeNativeMessageExtensions 11 | { 12 | /// 13 | /// Gets the customization of the outgoing native message sent using , or . 14 | /// 15 | /// Option being extended. 16 | /// The customization action or null. 17 | public static Action? GetNativeMessageCustomization(this ExtendableOptions options) 18 | => options.GetExtensions().TryGet>(NativeMessageCustomizationBehavior.CustomizationKey, out var customization) ? customization : null; 19 | } -------------------------------------------------------------------------------- /src/Transport/Utilities/TransactionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus; 2 | 3 | using System.Transactions; 4 | 5 | static class TransactionExtensions 6 | { 7 | public static TransactionScope ToScope(this Transaction? transaction) => 8 | transaction != null 9 | ? new TransactionScope(transaction, TransactionScopeAsyncFlowOption.Enabled) 10 | : new TransactionScope(TransactionScopeOption.Suppress, TransactionScopeAsyncFlowOption.Enabled); 11 | } -------------------------------------------------------------------------------- /src/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/TransportTests/ConfigureAzureServiceBusTransportInfrastructure.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 | public class ConfigureAzureServiceBusTransportInfrastructure : IConfigureTransportInfrastructure 9 | { 10 | public static readonly string ConnectionString = Environment.GetEnvironmentVariable("AzureServiceBus_ConnectionString"); 11 | 12 | public TransportDefinition CreateTransportDefinition() 13 | { 14 | if (string.IsNullOrEmpty(ConnectionString)) 15 | { 16 | throw new InvalidOperationException("envvar AzureServiceBus_ConnectionString not set"); 17 | } 18 | var transport = new AzureServiceBusTransport(ConnectionString, TopicTopology.Default); 19 | return transport; 20 | } 21 | 22 | public async Task Configure(TransportDefinition transportDefinition, HostSettings hostSettings, QueueAddress inputQueueName, string errorQueueName, CancellationToken cancellationToken = default) 23 | { 24 | var transportInfrastructure = await transportDefinition.Initialize( 25 | hostSettings, 26 | [ 27 | new ReceiveSettings(inputQueueName.ToString(), inputQueueName, true, false, errorQueueName) 28 | ], 29 | [], 30 | cancellationToken); 31 | 32 | return transportInfrastructure; 33 | } 34 | 35 | public Task Cleanup(CancellationToken cancellationToken = default) => Task.CompletedTask; 36 | } -------------------------------------------------------------------------------- /src/TransportTests/NServiceBus.Transport.AzureServiceBus.TransportTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net9.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 | 29 | 30 | 31 | 32 | 33 | AcceptanceTestExtensions.cs 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/TransportTests/When_MessageLockLostException_is_thrown_from_on_error.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.TransportTests 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using Azure.Messaging.ServiceBus; 6 | using NServiceBus.TransportTests; 7 | using NUnit.Framework; 8 | 9 | [TestFixture] 10 | public class When_MessageLockLostException_is_thrown_from_on_error : NServiceBusTransportTest 11 | { 12 | [TestCase(TransportTransactionMode.None)] 13 | [TestCase(TransportTransactionMode.ReceiveOnly)] 14 | [TestCase(TransportTransactionMode.SendsAtomicWithReceive)] 15 | public async Task Should_not_raise_critical_error(TransportTransactionMode transactionMode) 16 | { 17 | var onErrorCalled = CreateTaskCompletionSource(); 18 | string criticalErrorDetails = null; 19 | 20 | await StartPump( 21 | (_, __) => 22 | { 23 | throw new Exception("from onMessage"); 24 | }, 25 | (_, __) => 26 | { 27 | onErrorCalled.TrySetResult(true); 28 | throw new ServiceBusException("from onError", ServiceBusFailureReason.MessageLockLost); 29 | }, 30 | transactionMode, 31 | (msg, ex, ___) => 32 | { 33 | criticalErrorDetails = $"{msg}, Exception: {ex}"; 34 | } 35 | ); 36 | 37 | await SendMessage(InputQueueName); 38 | 39 | await onErrorCalled.Task; 40 | 41 | await StopPump(); 42 | 43 | Assert.That(criticalErrorDetails, Is.Null, $"Should not invoke critical error for {nameof(ServiceBusException)}"); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/TransportTests/When_ServiceBusTimeoutException_is_thrown_from_on_error.cs: -------------------------------------------------------------------------------- 1 | namespace NServiceBus.Transport.AzureServiceBus.TransportTests 2 | { 3 | using System; 4 | using System.Threading.Tasks; 5 | using Azure.Messaging.ServiceBus; 6 | using NServiceBus.TransportTests; 7 | using NUnit.Framework; 8 | 9 | [TestFixture] 10 | public class When_ServiceBusTimeoutException_is_thrown_from_on_error : NServiceBusTransportTest 11 | { 12 | [TestCase(TransportTransactionMode.None)] 13 | [TestCase(TransportTransactionMode.ReceiveOnly)] 14 | [TestCase(TransportTransactionMode.SendsAtomicWithReceive)] 15 | public async Task Should_not_raise_critical_error(TransportTransactionMode transactionMode) 16 | { 17 | var onErrorCalled = CreateTaskCompletionSource(); 18 | string criticalErrorDetails = null; 19 | 20 | await StartPump( 21 | (_, __) => 22 | { 23 | throw new Exception("from onMessage"); 24 | }, 25 | (_, __) => 26 | { 27 | onErrorCalled.TrySetResult(true); 28 | throw new ServiceBusException("from onError", ServiceBusFailureReason.ServiceTimeout); 29 | }, 30 | transactionMode, 31 | (msg, ex, ___) => 32 | { 33 | criticalErrorDetails = $"{msg}, Exception: {ex}"; 34 | } 35 | ); 36 | 37 | await SendMessage(InputQueueName); 38 | 39 | await onErrorCalled.Task; 40 | 41 | await StopPump(); 42 | 43 | Assert.That(criticalErrorDetails, Is.Null, $"Should not invoke critical error for {nameof(ServiceBusException)}"); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------