├── .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 |
--------------------------------------------------------------------------------