diff --git a/src/Mocha/src/Mocha.Transport.AzureServiceBus/Topology/AzureServiceBusRoutingStrategy.cs b/src/Mocha/src/Mocha.Transport.AzureServiceBus/Topology/AzureServiceBusRoutingStrategy.cs
index a7eb6d7e296..f11309252ce 100644
--- a/src/Mocha/src/Mocha.Transport.AzureServiceBus/Topology/AzureServiceBusRoutingStrategy.cs
+++ b/src/Mocha/src/Mocha.Transport.AzureServiceBus/Topology/AzureServiceBusRoutingStrategy.cs
@@ -289,23 +289,34 @@ public override void DiscoverTopology(
return;
}
- if (Transport.BindMode != MessagingBindMode.Implicit)
- {
- return;
- }
+ // Under explicit binding, a target that is not declared is recorded in the topology
+ // but never provisioned. The sender does not own it.
+ var bindImplicitly = Transport.BindMode == MessagingBindMode.Implicit;
if (azureConfiguration.TopicName is not null)
{
_topology.GetOrAddTopic(
azureConfiguration.TopicName,
- static _ => new AzureServiceBusTopicConfiguration());
+ bindImplicitly
+ ? static _ => new AzureServiceBusTopicConfiguration()
+ : static _ => new AzureServiceBusTopicConfiguration
+ {
+ AutoProvision = false,
+ Origin = TopologyOrigin.Endpoint
+ });
}
if (azureConfiguration.QueueName is not null)
{
_topology.GetOrAddQueue(
azureConfiguration.QueueName,
- static _ => new AzureServiceBusQueueConfiguration());
+ bindImplicitly
+ ? static _ => new AzureServiceBusQueueConfiguration()
+ : static _ => new AzureServiceBusQueueConfiguration
+ {
+ AutoProvision = false,
+ Origin = TopologyOrigin.Endpoint
+ });
}
}
diff --git a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQDispatchEndpointDescriptor.cs b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQDispatchEndpointDescriptor.cs
index bd9de26e7d5..e8da9ba6c52 100644
--- a/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQDispatchEndpointDescriptor.cs
+++ b/src/Mocha/src/Mocha.Transport.RabbitMQ/Descriptors/IRabbitMQDispatchEndpointDescriptor.cs
@@ -8,7 +8,6 @@ public interface IRabbitMQDispatchEndpointDescriptor
{
///
/// Sets the endpoint to dispatch messages to the specified queue, clearing any exchange target.
- /// A queue that is not declared in the transport topology is addressed by name and never provisioned.
///
/// The target queue name.
/// The descriptor for method chaining.
@@ -16,7 +15,6 @@ public interface IRabbitMQDispatchEndpointDescriptor
///
/// Sets the endpoint to dispatch messages to the specified exchange, clearing any queue target.
- /// An exchange that is not declared in the transport topology is addressed by name and never provisioned.
///
/// The target exchange name.
/// The descriptor for method chaining.
diff --git a/src/Mocha/src/Mocha.Transport.RabbitMQ/RabbitMQDispatchEndpoint.cs b/src/Mocha/src/Mocha.Transport.RabbitMQ/RabbitMQDispatchEndpoint.cs
index cb8f58a98dd..6918ff6f0c1 100644
--- a/src/Mocha/src/Mocha.Transport.RabbitMQ/RabbitMQDispatchEndpoint.cs
+++ b/src/Mocha/src/Mocha.Transport.RabbitMQ/RabbitMQDispatchEndpoint.cs
@@ -8,37 +8,17 @@ namespace Mocha.Transport.RabbitMQ;
/// RabbitMQ dispatch endpoint that publishes outbound messages to a target queue or exchange
/// using pooled channels from the transport's dispatcher.
///
-///
-/// The target is addressed by name. A target that is part of the transport topology is provisioned
-/// according to its auto-provision setting; a target outside the topology is published to by name
-/// and is never declared, bound, or consumed.
-///
/// The owning RabbitMQ transport instance.
public sealed class RabbitMQDispatchEndpoint(RabbitMQMessagingTransport transport)
: DispatchEndpoint(transport)
{
- private CachedString _exchangeName = CachedString.Empty;
- private CachedString _routingKey = CachedString.Empty;
-
- ///
- /// Gets the name of the target queue, or null if the endpoint targets an exchange.
- ///
- public string? QueueName { get; private set; }
-
- ///
- /// Gets the name of the target exchange, or null if the endpoint targets a queue.
- ///
- public string? ExchangeName { get; private set; }
-
///
- /// Gets the topology queue this endpoint targets, or null if the endpoint targets an
- /// exchange or a queue that is not part of the transport topology.
+ /// Gets the target queue for this endpoint, or null if the endpoint targets an exchange.
///
public RabbitMQQueue? Queue { get; private set; }
///
- /// Gets the topology exchange this endpoint targets, or null if the endpoint targets a
- /// queue or an exchange that is not part of the transport topology.
+ /// Gets the target exchange for this endpoint, or null if the endpoint targets a queue.
///
public RabbitMQExchange? Exchange { get; private set; }
@@ -127,14 +107,19 @@ private async ValueTask DispatchAsync(
}
else
{
- exchangeName = _exchangeName;
- routingKey = _routingKey;
+ if (Exchange is not null)
+ {
+ exchangeName = Exchange.CachedName;
- if (ExchangeName is not null
- && envelope.Headers is not null
- && envelope.Headers.TryGet(RabbitMQMessageHeaders.RoutingKey, out var rk))
+ if (envelope.Headers is not null
+ && envelope.Headers.TryGet(RabbitMQMessageHeaders.RoutingKey, out var rk))
+ {
+ routingKey = new CachedString(rk);
+ }
+ }
+ else if (Queue is not null)
{
- routingKey = new CachedString(rk);
+ routingKey = Queue.CachedName;
}
}
@@ -182,21 +167,22 @@ protected override void OnComplete(
RabbitMQDispatchEndpointConfiguration configuration)
{
var topology = (RabbitMQMessagingTopology)Transport.Topology;
-
- if (configuration.ExchangeName is { } exchangeName)
+ if (configuration.ExchangeName is not null)
{
- ExchangeName = exchangeName;
- Exchange = topology.Exchanges.FirstOrDefault(e => e.Name == exchangeName);
- _exchangeName = Exchange?.CachedName ?? new CachedString(exchangeName);
+ Exchange =
+ topology.Exchanges.FirstOrDefault(e => e.Name == configuration.ExchangeName)
+ ?? throw new InvalidOperationException("Exchange not found");
}
- else if (configuration.QueueName is { } queueName)
+ else if (configuration.QueueName is not null)
{
- QueueName = queueName;
- Queue = topology.Queues.FirstOrDefault(q => q.Name == queueName);
- _routingKey = Queue?.CachedName ?? new CachedString(queueName);
+ Queue =
+ topology.Queues.FirstOrDefault(q => q.Name == configuration.QueueName)
+ ?? throw new InvalidOperationException("Queue not found");
}
- // The destination stays unset for a target outside the topology.
- Destination = ((TopologyResource?)Exchange ?? Queue)!;
+ Destination =
+ Exchange as TopologyResource
+ ?? Queue as TopologyResource
+ ?? throw new InvalidOperationException("Destination is not set");
}
}
diff --git a/src/Mocha/src/Mocha.Transport.RabbitMQ/RabbitMQMessagingTransport.cs b/src/Mocha/src/Mocha.Transport.RabbitMQ/RabbitMQMessagingTransport.cs
index 279492437fe..8834858d668 100644
--- a/src/Mocha/src/Mocha.Transport.RabbitMQ/RabbitMQMessagingTransport.cs
+++ b/src/Mocha/src/Mocha.Transport.RabbitMQ/RabbitMQMessagingTransport.cs
@@ -240,7 +240,12 @@ public override bool TryGetDispatchEndpoint(Uri address, [NotNullWhen(true)] out
{
foreach (var candidate in DispatchEndpoints)
{
- if (candidate.IsCompleted && candidate.Address == address)
+ if (!candidate.IsCompleted)
+ {
+ continue;
+ }
+
+ if (candidate.Address == address)
{
endpoint = candidate;
return true;
@@ -248,35 +253,33 @@ public override bool TryGetDispatchEndpoint(Uri address, [NotNullWhen(true)] out
}
}
- string? queueName = null;
- string? exchangeName = null;
-
- if (address.TryParseTopologyAddress(Topology.Address, out var kind, out var name))
+ if (Topology.Address.IsBaseOf(address))
{
- if (kind is 'q')
- {
- queueName = name;
- }
- else if (kind is 'e')
+ foreach (var candidate in DispatchEndpoints)
{
- exchangeName = name;
+ if (!candidate.IsCompleted)
+ {
+ continue;
+ }
+
+ if (candidate.Destination.Address == address)
+ {
+ endpoint = candidate;
+ return true;
+ }
}
}
- else if (TryGetResourceName(address, "queue", out var resourceQueueName))
- {
- queueName = resourceQueueName;
- }
- else if (TryGetResourceName(address, "exchange", out var resourceExchangeName))
- {
- exchangeName = resourceExchangeName;
- }
- if (queueName is not null)
+ if (TryGetResourceName(address, "queue", out var queueName))
{
foreach (var candidate in DispatchEndpoints)
{
- if (candidate is RabbitMQDispatchEndpoint { IsCompleted: true } rabbitCandidate
- && rabbitCandidate.QueueName == queueName)
+ if (!candidate.IsCompleted)
+ {
+ continue;
+ }
+
+ if (candidate.Destination is RabbitMQQueue queue && queue.Name == queueName)
{
endpoint = candidate;
return true;
@@ -284,12 +287,17 @@ public override bool TryGetDispatchEndpoint(Uri address, [NotNullWhen(true)] out
}
}
- if (exchangeName is not null)
+ if (TryGetResourceName(address, "exchange", out var exchangeName))
{
foreach (var candidate in DispatchEndpoints)
{
- if (candidate is RabbitMQDispatchEndpoint { IsCompleted: true } rabbitCandidate
- && rabbitCandidate.ExchangeName == exchangeName)
+ if (!candidate.IsCompleted)
+ {
+ continue;
+ }
+
+ if (candidate.Destination is RabbitMQExchange exchange
+ && exchange.Name == exchangeName)
{
endpoint = candidate;
return true;
diff --git a/src/Mocha/src/Mocha.Transport.RabbitMQ/Topology/RabbitMQRoutingStrategy.cs b/src/Mocha/src/Mocha.Transport.RabbitMQ/Topology/RabbitMQRoutingStrategy.cs
index 14cc293e1cd..f1c5f12f9fd 100644
--- a/src/Mocha/src/Mocha.Transport.RabbitMQ/Topology/RabbitMQRoutingStrategy.cs
+++ b/src/Mocha/src/Mocha.Transport.RabbitMQ/Topology/RabbitMQRoutingStrategy.cs
@@ -91,7 +91,7 @@ public sealed class RabbitMQRoutingStrategy : RoutingStrategy new RabbitMQExchangeConfiguration());
+ bindImplicitly
+ ? static _ => new RabbitMQExchangeConfiguration()
+ : static _ => new RabbitMQExchangeConfiguration
+ {
+ AutoProvision = false,
+ Origin = TopologyOrigin.Endpoint
+ });
}
- if (rabbitConfiguration.QueueName is not null && bindImplicitly)
+ if (rabbitConfiguration.QueueName is not null)
{
_topology.GetOrAddQueue(
rabbitConfiguration.QueueName,
- _ => new RabbitMQQueueConfiguration { AutoProvision = rabbitConfiguration.AutoProvision });
+ bindImplicitly
+ ? _ => new RabbitMQQueueConfiguration { AutoProvision = rabbitConfiguration.AutoProvision }
+ : static _ => new RabbitMQQueueConfiguration
+ {
+ AutoProvision = false,
+ Origin = TopologyOrigin.Endpoint
+ });
}
var schema = Transport.Schema;
@@ -531,7 +540,7 @@ private bool TryGetQueueName(
}
}
- if (address.TryParseTopologyAddress(Transport.Topology.Address, out var topologyKind, out var topologyName)
+ if (TryParseTopologyAddress(address, out var topologyKind, out var topologyName)
&& topologyKind is 'q')
{
queueName = topologyName;
@@ -547,6 +556,75 @@ private bool TryGetQueueName(
queueName = string.Empty;
return false;
}
+
+ ///
+ /// Parses an address under the transport topology into its kind and resource name.
+ /// Everything after the kind segment is the name, because a queue or exchange name may
+ /// contain a slash.
+ ///
+ private bool TryParseTopologyAddress(
+ Uri address,
+ out char kind,
+ out string name)
+ {
+ kind = default;
+ name = string.Empty;
+
+ // A valid resource path is not enough to identify this transport. For example,
+ // rabbitmq://other-host:5672/tenant-a/q/orders must not be claimed by this topology.
+ var topologyAddress = Transport.Topology.Address;
+ if (!address.Scheme.EqualsOrdinalIgnoreCase(topologyAddress.Scheme)
+ || !address.Host.EqualsOrdinalIgnoreCase(topologyAddress.Host)
+ || address.Port != topologyAddress.Port)
+ {
+ return false;
+ }
+
+ // Normalize only the topology path: "/" becomes empty and "/tenant-a/" becomes
+ // "/tenant-a". Keep the address path unchanged while removing the topology prefix below.
+ var basePath = topologyAddress.AbsolutePath.AsSpan().TrimEnd('/');
+ var path = address.AbsolutePath.AsSpan();
+ ReadOnlySpan relativePath;
+
+ if (basePath.IsEmpty)
+ {
+ // A root topology consumes exactly one structural slash:
+ // "/q/orders" becomes "q/orders". The shorthand "rabbitmq:q/orders" has no
+ // leading slash and is handled by the transport-specific branch above.
+ if (path.IsEmpty || path[0] is not '/')
+ {
+ return false;
+ }
+
+ relativePath = path[1..];
+ }
+ else
+ {
+ // Remove the exact virtual-host path and its separator:
+ // "/tenant-a/q/orders" becomes "q/orders". Requiring the separator prevents
+ // "/tenant-a" from matching "/tenant-a-other/q/orders".
+ if (path.Length <= basePath.Length
+ || !path.StartsWith(basePath, StringComparison.Ordinal)
+ || path[basePath.Length] is not '/')
+ {
+ return false;
+ }
+
+ relativePath = path[(basePath.Length + 1)..];
+ }
+
+ // Require "{kind}/{name}", where kind is one character. Everything after the first
+ // slash is the name, so a name containing a slash, such as "q/nested/queue", stays intact.
+ if (relativePath.Length < 3 || relativePath[1] is not '/')
+ {
+ return false;
+ }
+
+ kind = relativePath[0];
+ // AbsolutePath is escaped, so "q/space%20name" must produce "space name".
+ name = Uri.UnescapeDataString(relativePath[2..].ToString());
+ return true;
+ }
}
file static class Extensions
diff --git a/src/Mocha/src/Mocha.Transport.RabbitMQ/UriHelpers.cs b/src/Mocha/src/Mocha.Transport.RabbitMQ/UriHelpers.cs
index 5e354a34848..f7b27d87932 100644
--- a/src/Mocha/src/Mocha.Transport.RabbitMQ/UriHelpers.cs
+++ b/src/Mocha/src/Mocha.Transport.RabbitMQ/UriHelpers.cs
@@ -31,77 +31,4 @@ public static bool TryGetRoutingKey(this Uri uri, [NotNullWhen(true)] out string
routingKey = null;
return false;
}
-
- ///
- /// Attempts to parse an address under the given topology address into its resource kind and name.
- /// Everything after the kind segment is the name, because a queue or exchange name may contain a slash.
- ///
- /// The address to parse.
- /// The transport topology address the resource must belong to.
- /// When this method returns true, contains the single-character resource kind, such as q or e.
- /// When this method returns true, contains the unescaped resource name.
- /// true if the address names a resource under the topology address; otherwise, false.
- public static bool TryParseTopologyAddress(
- this Uri address,
- Uri topologyAddress,
- out char kind,
- out string name)
- {
- kind = default;
- name = string.Empty;
-
- // A valid resource path is not enough to identify this transport. For example,
- // rabbitmq://other-host:5672/tenant-a/q/orders must not be claimed by this topology.
- if (!address.Scheme.EqualsOrdinalIgnoreCase(topologyAddress.Scheme)
- || !address.Host.EqualsOrdinalIgnoreCase(topologyAddress.Host)
- || address.Port != topologyAddress.Port)
- {
- return false;
- }
-
- // Normalize only the topology path: "/" becomes empty and "/tenant-a/" becomes
- // "/tenant-a". Keep the address path unchanged while removing the topology prefix below.
- var basePath = topologyAddress.AbsolutePath.AsSpan().TrimEnd('/');
- var path = address.AbsolutePath.AsSpan();
- ReadOnlySpan relativePath;
-
- if (basePath.IsEmpty)
- {
- // A root topology consumes exactly one structural slash:
- // "/q/orders" becomes "q/orders". The shorthand "rabbitmq:q/orders" has no
- // leading slash and is handled by the transport-specific branch of the caller.
- if (path.IsEmpty || path[0] is not '/')
- {
- return false;
- }
-
- relativePath = path[1..];
- }
- else
- {
- // Remove the exact virtual-host path and its separator:
- // "/tenant-a/q/orders" becomes "q/orders". Requiring the separator prevents
- // "/tenant-a" from matching "/tenant-a-other/q/orders".
- if (path.Length <= basePath.Length
- || !path.StartsWith(basePath, StringComparison.Ordinal)
- || path[basePath.Length] is not '/')
- {
- return false;
- }
-
- relativePath = path[(basePath.Length + 1)..];
- }
-
- // Require "{kind}/{name}", where kind is one character. Everything after the first
- // slash is the name, so a name containing a slash, such as "q/nested/queue", stays intact.
- if (relativePath.Length < 3 || relativePath[1] is not '/')
- {
- return false;
- }
-
- kind = relativePath[0];
- // AbsolutePath is escaped, so "q/space%20name" must produce "space name".
- name = Uri.UnescapeDataString(relativePath[2..].ToString());
- return true;
- }
}
diff --git a/src/Mocha/src/Mocha/Endpoints/DispatchEndpoint.cs b/src/Mocha/src/Mocha/Endpoints/DispatchEndpoint.cs
index 9b76620be6b..e9e9c729718 100644
--- a/src/Mocha/src/Mocha/Endpoints/DispatchEndpoint.cs
+++ b/src/Mocha/src/Mocha/Endpoints/DispatchEndpoint.cs
@@ -95,7 +95,6 @@ protected DispatchEndpoint(MessagingTransport transport)
///
/// Gets the topology resource that represents the destination to which messages are dispatched.
- /// A transport leaves this unset when the endpoint addresses a destination outside its topology.
///
public TopologyResource Destination { get; protected set; } = null!;
diff --git a/src/Mocha/test/Mocha.Transport.AzureServiceBus.Tests/Routing/AzureServiceBusReceiveTopologyTests.cs b/src/Mocha/test/Mocha.Transport.AzureServiceBus.Tests/Routing/AzureServiceBusReceiveTopologyTests.cs
index 58760f1fd55..6f798dcdc21 100644
--- a/src/Mocha/test/Mocha.Transport.AzureServiceBus.Tests/Routing/AzureServiceBusReceiveTopologyTests.cs
+++ b/src/Mocha/test/Mocha.Transport.AzureServiceBus.Tests/Routing/AzureServiceBusReceiveTopologyTests.cs
@@ -95,23 +95,26 @@ public void DiscoverTopology_Should_SubscribeCustomDispatchTopic_When_PublishRou
public void DiscoverTopology_Should_NotProvisionTopic_When_QueueBindsExplicitlyAndTopicIsUndeclared()
{
// arrange
- // Receive topology discovery runs before dispatch topology discovery, so an unguarded
- // EnsureTopic on the receive side would implicitly create "custom-orders" and mask the
- // fact that it was never declared, letting the explicit-mode dispatch endpoint complete
- // successfully against a topic the user never provisioned.
-
- // act
- var exception = Record.Exception(() => CreateRuntime(
+ var runtime = CreateRuntime(
b => b.AddConsumer(),
t =>
{
t.BindExplicitly();
t.Queue("orders").Consumer();
t.DispatchEndpoint("custom-orders-endpoint").ToTopic("custom-orders").Publish();
- }));
+ });
+ var (topology, endpoint) = ResolveConsumerEndpoint(runtime);
+
+ // act
+ var topic = topology.Topics.Single(t => t.Name == "custom-orders");
+ var subscriptions = topology.Subscriptions
+ .Where(s => s.Source.Name == "custom-orders" && s.Destination.Name == endpoint.Queue.Name)
+ .ToList();
// assert
- Assert.Equal("Topic not found", Assert.IsType(exception).Message);
+ Assert.False(topic.AutoProvision);
+ Assert.Equal(TopologyOrigin.Endpoint, topic.Origin);
+ Assert.Empty(subscriptions);
}
[Fact]
diff --git a/src/Mocha/test/Mocha.Transport.AzureServiceBus.Tests/Topology/AzureServiceBusDispatchTargetTests.cs b/src/Mocha/test/Mocha.Transport.AzureServiceBus.Tests/Topology/AzureServiceBusDispatchTargetTests.cs
new file mode 100644
index 00000000000..3a5b45419dd
--- /dev/null
+++ b/src/Mocha/test/Mocha.Transport.AzureServiceBus.Tests/Topology/AzureServiceBusDispatchTargetTests.cs
@@ -0,0 +1,113 @@
+using CookieCrumble;
+using Microsoft.Extensions.DependencyInjection;
+using Mocha.Transport.AzureServiceBus.Tests.Helpers;
+
+namespace Mocha.Transport.AzureServiceBus.Tests.Topology;
+
+public class AzureServiceBusDispatchTargetTests
+{
+ private const string DummyConnectionString =
+ "Endpoint=sb://localhost/;SharedAccessKeyName=test;SharedAccessKey=test";
+
+ [Fact]
+ public void GetDispatchEndpoint_Should_AddUnprovisionedQueue_When_QueueNotDeclaredUnderExplicitBinding()
+ {
+ // arrange
+ var runtime = CreateRuntime(t => t.BindExplicitly());
+ var transport = runtime.Transports.OfType().Single();
+
+ // act
+ var endpoint = runtime.GetDispatchEndpoint(new Uri("azuresb:q/reporting.events"));
+
+ // assert
+ var azureEndpoint = Assert.IsType(endpoint);
+ Assert.NotNull(azureEndpoint.Queue);
+ Assert.False(azureEndpoint.Queue.AutoProvision);
+ Assert.Equal(TopologyOrigin.Endpoint, azureEndpoint.Queue.Origin);
+ Assert.Same(azureEndpoint.Queue, azureEndpoint.Destination);
+ AzureServiceBusDescribeSnapshot.Create(transport.Describe()).MatchInlineSnapshot(
+ """
+ {
+ "Schema": "azuresb",
+ "TransportType": "AzureServiceBusMessagingTransport",
+ "Entities": [
+ {
+ "Kind": "queue",
+ "Name": "reporting.events",
+ "AutoProvision": false,
+ "Origin": "endpoint"
+ }
+ ],
+ "Links": []
+ }
+ """);
+ }
+
+ [Fact]
+ public void GetDispatchEndpoint_Should_AddUnprovisionedTopic_When_TopicNotDeclaredUnderExplicitBinding()
+ {
+ // arrange
+ var runtime = CreateRuntime(t => t.BindExplicitly());
+ var transport = runtime.Transports.OfType().Single();
+
+ // act
+ var endpoint = runtime.GetDispatchEndpoint(new Uri("azuresb:t/reporting-events"));
+
+ // assert
+ var azureEndpoint = Assert.IsType(endpoint);
+ Assert.NotNull(azureEndpoint.Topic);
+ Assert.False(azureEndpoint.Topic.AutoProvision);
+ Assert.Equal(TopologyOrigin.Endpoint, azureEndpoint.Topic.Origin);
+ Assert.Same(azureEndpoint.Topic, azureEndpoint.Destination);
+ AzureServiceBusDescribeSnapshot.Create(transport.Describe()).MatchInlineSnapshot(
+ """
+ {
+ "Schema": "azuresb",
+ "TransportType": "AzureServiceBusMessagingTransport",
+ "Entities": [
+ {
+ "Kind": "topic",
+ "Name": "reporting-events",
+ "AutoProvision": false,
+ "Origin": "endpoint"
+ }
+ ],
+ "Links": []
+ }
+ """);
+ }
+
+ [Fact]
+ public void GetDispatchEndpoint_Should_BindDeclaredQueue_When_QueueDeclaredUnderExplicitBinding()
+ {
+ // arrange
+ var runtime = CreateRuntime(t =>
+ {
+ t.BindExplicitly();
+ t.DeclareQueue("reporting.events");
+ });
+
+ // act
+ var endpoint = runtime.GetDispatchEndpoint(new Uri("azuresb:q/reporting.events"));
+
+ // assert
+ var azureEndpoint = Assert.IsType(endpoint);
+ Assert.NotNull(azureEndpoint.Queue);
+ Assert.Equal(TopologyOrigin.Declared, azureEndpoint.Queue.Origin);
+ Assert.Same(azureEndpoint.Queue, azureEndpoint.Destination);
+ }
+
+ private static MessagingRuntime CreateRuntime(
+ Action configureTransport)
+ {
+ var services = new ServiceCollection();
+ var builder = services.AddMessageBus();
+ return builder
+ .AddAzureServiceBus(t =>
+ {
+ t.ConnectionString(DummyConnectionString);
+ configureTransport(t);
+ })
+ .BuildRuntime();
+ }
+}
diff --git a/src/Mocha/test/Mocha.Transport.RabbitMQ.Tests/RabbitMQDispatchEndpointTests.cs b/src/Mocha/test/Mocha.Transport.RabbitMQ.Tests/RabbitMQDispatchEndpointTests.cs
index d54e6d05ef8..35f08956a75 100644
--- a/src/Mocha/test/Mocha.Transport.RabbitMQ.Tests/RabbitMQDispatchEndpointTests.cs
+++ b/src/Mocha/test/Mocha.Transport.RabbitMQ.Tests/RabbitMQDispatchEndpointTests.cs
@@ -25,7 +25,6 @@ public void DispatchEndpoint_Should_TargetQueue_When_QueueConfigured()
// assert
Assert.NotNull(endpoint.Queue);
Assert.Equal("my-q", endpoint.Queue.Name);
- Assert.Equal("my-q", endpoint.QueueName);
Assert.Null(endpoint.Exchange);
Assert.IsType(endpoint.Destination);
}
@@ -49,7 +48,6 @@ public void DispatchEndpoint_Should_TargetExchange_When_ExchangeConfigured()
// assert
Assert.NotNull(endpoint.Exchange);
Assert.Equal("my-ex", endpoint.Exchange.Name);
- Assert.Equal("my-ex", endpoint.ExchangeName);
Assert.Null(endpoint.Queue);
Assert.IsType(endpoint.Destination);
}
@@ -201,11 +199,9 @@ public void DispatchEndpoint_Should_SetDestinationAddress_When_ReplyPathContains
}
[Fact]
- public void GetDispatchEndpoint_Should_TargetQueueByName_When_QueueNotInTopology()
+ public void GetDispatchEndpoint_Should_AddUnprovisionedQueue_When_QueueNotDeclaredUnderExplicitBinding()
{
// arrange
- // explicit binding adds nothing to the topology for an address-created endpoint, so the
- // queue is addressed by name only
var runtime = CreateRuntime(t => t.BindExplicitly());
var transport = runtime.Transports.OfType().Single();
@@ -214,22 +210,30 @@ public void GetDispatchEndpoint_Should_TargetQueueByName_When_QueueNotInTopology
// assert
var rabbitEndpoint = Assert.IsType(endpoint);
- Assert.Equal("reporting.events", rabbitEndpoint.QueueName);
- Assert.Null(rabbitEndpoint.Queue);
- Assert.Null(rabbitEndpoint.Destination);
+ Assert.NotNull(rabbitEndpoint.Queue);
+ Assert.False(rabbitEndpoint.Queue.AutoProvision);
+ Assert.Equal(TopologyOrigin.Endpoint, rabbitEndpoint.Queue.Origin);
+ Assert.Same(rabbitEndpoint.Queue, rabbitEndpoint.Destination);
RabbitMQDescribeSnapshot.Create(transport.Describe()).MatchInlineSnapshot(
"""
{
"Schema": "rabbitmq",
"TransportType": "RabbitMQMessagingTransport",
- "Entities": [],
+ "Entities": [
+ {
+ "Kind": "queue",
+ "Name": "reporting.events",
+ "AutoProvision": false,
+ "Origin": "endpoint"
+ }
+ ],
"Links": []
}
""");
}
[Fact]
- public void GetDispatchEndpoint_Should_TargetExchangeByName_When_ExchangeNotInTopology()
+ public void GetDispatchEndpoint_Should_AddUnprovisionedExchange_When_ExchangeNotDeclaredUnderExplicitBinding()
{
// arrange
var runtime = CreateRuntime(t => t.BindExplicitly());
@@ -240,15 +244,23 @@ public void GetDispatchEndpoint_Should_TargetExchangeByName_When_ExchangeNotInTo
// assert
var rabbitEndpoint = Assert.IsType(endpoint);
- Assert.Equal("reporting-events", rabbitEndpoint.ExchangeName);
- Assert.Null(rabbitEndpoint.Exchange);
- Assert.Null(rabbitEndpoint.Destination);
+ Assert.NotNull(rabbitEndpoint.Exchange);
+ Assert.False(rabbitEndpoint.Exchange.AutoProvision);
+ Assert.Equal(TopologyOrigin.Endpoint, rabbitEndpoint.Exchange.Origin);
+ Assert.Same(rabbitEndpoint.Exchange, rabbitEndpoint.Destination);
RabbitMQDescribeSnapshot.Create(transport.Describe()).MatchInlineSnapshot(
"""
{
"Schema": "rabbitmq",
"TransportType": "RabbitMQMessagingTransport",
- "Entities": [],
+ "Entities": [
+ {
+ "Kind": "exchange",
+ "Name": "reporting-events",
+ "AutoProvision": false,
+ "Origin": "endpoint"
+ }
+ ],
"Links": []
}
""");
diff --git a/website/content/docs/mocha/routing-and-endpoints.md b/website/content/docs/mocha/routing-and-endpoints.md
index d794c5bf19c..d197d8ef472 100644
--- a/website/content/docs/mocha/routing-and-endpoints.md
+++ b/website/content/docs/mocha/routing-and-endpoints.md
@@ -219,7 +219,7 @@ new SendOptions
cancellationToken);
```
-The address does not have to be part of the transport topology. On RabbitMQ, `queue:` or the transport form `rabbitmq:q/` reaches a queue the host never declared, for example a queue another service owns. The host declares, binds, and consumes nothing for that queue. As with every RabbitMQ publish, a message the broker cannot route, for example because the queue does not exist, is dropped.
+The queue does not have to be declared by the host. On RabbitMQ, `queue:` or the transport form `rabbitmq:q/` reaches a queue the host never declared, for example a queue another service owns. Under explicit binding, the host records that queue in its topology with auto-provisioning disabled and never declares, binds, or consumes it. A message the broker cannot route, for example because the queue does not exist, is dropped.
# Customize queues and binding