From 9a02c399e6936b9e6decc59f491a75f91837a588 Mon Sep 17 00:00:00 2001 From: "Jeremy D. Miller" Date: Sun, 1 Dec 2024 17:45:07 -0600 Subject: [PATCH] Docs for multi-tenancy usage with Azure Service Bus --- docs/.vitepress/config.mts | 3 +- .../azureservicebus/multi-tenancy.md | 59 +++++++++++++++ .../Samples.cs | 71 +++++++++++++++++++ .../AzureServiceBusConfiguration.cs | 38 ++++++++++ .../multi_tenancy_through_virtual_hosts.cs | 2 +- .../Internal/RabbitMqTransportExpression.cs | 1 - 6 files changed, 171 insertions(+), 3 deletions(-) create mode 100644 docs/guide/messaging/transports/azureservicebus/multi-tenancy.md diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 150ee47ab..092ecadab 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -139,7 +139,8 @@ const config: UserConfig = { {text: 'Topics and Subscriptions', link:'/guide/messaging/transports/azureservicebus/topics'}, {text: 'Interoperability', link:'/guide/messaging/transports/azureservicebus/interoperability'}, {text: 'Session Identifiers and FIFO Queues', link: '/guide/messaging/transports/azureservicebus/session-identifiers'}, - {text: 'Scheduled Delivery', link: '/guide/messaging/transports/azureservicebus/scheduled'} + {text: 'Scheduled Delivery', link: '/guide/messaging/transports/azureservicebus/scheduled'}, + {text: 'Multi-Tenancy', link: '/guide/messaging/transports/azureservicebus/multi-tenancy'} ]}, {text: 'Amazon SQS', link: '/guide/messaging/transports/sqs/', items:[ {text: 'Publishing', link:'/guide/messaging/transports/sqs/publishing'}, diff --git a/docs/guide/messaging/transports/azureservicebus/multi-tenancy.md b/docs/guide/messaging/transports/azureservicebus/multi-tenancy.md new file mode 100644 index 000000000..1b3f5b968 --- /dev/null +++ b/docs/guide/messaging/transports/azureservicebus/multi-tenancy.md @@ -0,0 +1,59 @@ +# Multi-Tenancy with Azure Service Bus + +Let's take a trip to the world of IoT where you might very well build a single cloud hosted service that needs +to communicate via Rabbit MQ with devices at your customers sites. You'd preferably like to keep traffic separate +so that one customer never accidentally receives information from another customer. In this case, Wolverine now +lets you register separate Rabbit MQ brokers -- or at least separate virtual hosts within a single Rabbit MQ broker -- +for each tenant. + +::: info +Definitely see [Multi-Tenancy with Wolverine](/guide/handlers/multi-tenancy) for more information about how +Wolverine tracks the tenant id across messages. +::: + +Let's just jump straight into a simple example of the configuration: + +snippet: sample_configuring_azure_service_bus_for_multi_tenancy + +::: warning +Wolverine has no way of creating new Azure Service Bus namespaces for you +::: + +In the code sample above, I'm setting up the Azure Service Bus transport to "know" that there are multiple tenants +with separate Azure Service Bus fully qualified namespaces. + +::: tip +Note that Wolverine uses the credentials specified for the default Azure Service +Bus connection for all tenant specific connections +::: + +At runtime, if we send a message like so: + + + +```cs +public static async Task send_message_to_specific_tenant(IMessageBus bus) +{ + // Send a message tagged to a specific tenant id + await bus.PublishAsync(new Message1(), new DeliveryOptions { TenantId = "two" }); +} +``` +snippet source | anchor + + +In the case above, in the Wolverine internals, it: + +1. Routes the message to a Azure Service Bus queue named "outgoing" +2. Within the sender for that queue, Wolverine sees that `TenantId == "two"`, so it sends the message to the "outgoing" queue + on the Azure Service Bus connection that we specified for the "two" tenant id. + +Likewise, see the listening set up against the "incoming" queue above. At runtime, this Wolverine application will be +listening to a queue named "incoming" on the default Azure Service Bus namespace and a separate queue named "incoming" on the separate +fully qualified namespaces for the known tenants. When a message is received at any of these queues, it's tagged with the +`TenantId` that's appropriate for each separate tenant-specific listening endpoint. That helps Wolverine also track +tenant specific operations (with Marten maybe?) and tracks the tenant id across any outgoing messages or responses as well. + + + + + diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/Samples.cs b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/Samples.cs index 124b8705c..ab1ac6102 100644 --- a/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/Samples.cs +++ b/src/Transports/Azure/Wolverine.AzureServiceBus.Tests/Samples.cs @@ -1,9 +1,12 @@ using Azure.Messaging.ServiceBus; using JasperFx.Core; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Oakton.Resources; using Wolverine.ComplianceTests.Compliance; +using Wolverine.Transports.Sending; using Wolverine.Util; +using Xunit; namespace Wolverine.AzureServiceBus.Tests; @@ -115,6 +118,74 @@ public static async Task configure_custom_mappers() #endregion } + +public class multi_tenanted_brokers +{ + [Fact] + public void show_bootstrapping() + { + #region sample_configuring_azure_service_bus_for_multi_tenancy + + var builder = Host.CreateApplicationBuilder(); + + builder.UseWolverine(opts => + { + // One way or another, you're probably pulling the Azure Service Bus + // connection string out of configuration + var azureServiceBusConnectionString = builder + .Configuration + .GetConnectionString("azure-service-bus"); + + // Connect to the broker in the simplest possible way + opts.UseAzureServiceBus(azureServiceBusConnectionString) + + // This is the default, if there is no tenant id on an outgoing message, + // use the default broker + .TenantIdBehavior(TenantedIdBehavior.FallbackToDefault) + + // Or tell Wolverine instead to just quietly ignore messages sent + // to unrecognized tenant ids + .TenantIdBehavior(TenantedIdBehavior.IgnoreUnknownTenants) + + // Or be draconian and make Wolverine assert and throw an exception + // if an outgoing message does not have a tenant id + .TenantIdBehavior(TenantedIdBehavior.TenantIdRequired) + + // Add new tenants by registering the tenant id and a separate fully qualified namespace + // to a different Azure Service Bus connection + .AddTenantByNamespace("one", builder.Configuration.GetValue("asb_ns_one")) + .AddTenantByNamespace("two", builder.Configuration.GetValue("asb_ns_two")) + .AddTenantByNamespace("three", builder.Configuration.GetValue("asb_ns_three")) + + // OR, instead, add tenants by registering the tenant id and a separate connection string + // to a different Azure Service Bus connection + .AddTenantByConnectionString("four", builder.Configuration.GetConnectionString("asb_four")) + .AddTenantByConnectionString("five", builder.Configuration.GetConnectionString("asb_five")) + .AddTenantByConnectionString("six", builder.Configuration.GetConnectionString("asb_six")); + + // This Wolverine application would be listening to a queue + // named "incoming" on all Azure Service Bus connections, including the default + opts.ListenToAzureServiceBusQueue("incoming"); + + // This Wolverine application would listen to a single queue + // at the default connection regardless of tenant + opts.ListenToAzureServiceBusQueue("incoming_global") + .GlobalListener(); + + // Likewise, you can override the queue, subscription, and topic behavior + // to be "global" for all tenants with this syntax: + opts.PublishMessage() + .ToAzureServiceBusQueue("message1") + .GlobalSender(); + + opts.PublishMessage() + .ToAzureServiceBusTopic("message2") + .GlobalSender(); + }); + + #endregion + } +} } #region sample_custom_azure_service_bus_mapper diff --git a/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusConfiguration.cs b/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusConfiguration.cs index 8832e0efe..0f210cb20 100644 --- a/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusConfiguration.cs +++ b/src/Transports/Azure/Wolverine.AzureServiceBus/AzureServiceBusConfiguration.cs @@ -3,6 +3,7 @@ using Wolverine.AzureServiceBus.Internal; using Wolverine.Configuration; using Wolverine.Transports; +using Wolverine.Transports.Sending; namespace Wolverine.AzureServiceBus; @@ -26,6 +27,43 @@ protected override AzureServiceBusQueueSubscriberConfiguration createSubscriberE { return new AzureServiceBusQueueSubscriberConfiguration(subscriberEndpoint); } + + /// + /// Override the sending logic behavior for unknown or missing tenant ids when + /// using multi-tenanted namespaces + /// + /// + /// + public AzureServiceBusConfiguration TenantIdBehavior(TenantedIdBehavior tenantedIdBehavior) + { + Transport.TenantedIdBehavior = tenantedIdBehavior; + return this; + } + + /// + /// Add a connection to a different Azure Service Bus broker for the named tenant using a fully + /// qualified namespace + /// + /// + /// + /// + public AzureServiceBusConfiguration AddTenantByNamespace(string tenantId, string fullyQualifiedNamespace) + { + Transport.Tenants[tenantId].Transport.FullyQualifiedNamespace = fullyQualifiedNamespace; + return this; + } + + /// + /// Add a connection to a different Azure Service Bus broker for the named tenant using a connection string + /// + /// + /// + /// + public AzureServiceBusConfiguration AddTenantByConnectionString(string tenantId, string connectionString) + { + Transport.Tenants[tenantId].Transport.ConnectionString = connectionString; + return this; + } /// /// Add explicit configuration to an AzureServiceBus queue that is being created by diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/multi_tenancy_through_virtual_hosts.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/multi_tenancy_through_virtual_hosts.cs index f312f4592..4557cb0ba 100644 --- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/multi_tenancy_through_virtual_hosts.cs +++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ.Tests/multi_tenancy_through_virtual_hosts.cs @@ -303,7 +303,7 @@ public static async Task Configure() // More on this in the docs.... opts.PublishMessage() - .ToRabbitQueue("outgoing"); + .ToRabbitQueue("outgoing").GlobalSender(); }); #endregion diff --git a/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqTransportExpression.cs b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqTransportExpression.cs index 0ad42ca31..e6ea05a51 100644 --- a/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqTransportExpression.cs +++ b/src/Transports/RabbitMQ/Wolverine.RabbitMQ/Internal/RabbitMqTransportExpression.cs @@ -42,7 +42,6 @@ public RabbitMqTransportExpression TenantIdBehavior(TenantedIdBehavior tenantedI /// /// /// - /// public RabbitMqTransportExpression AddTenant(string tenantId, string virtualHostName) { Transport.Tenants[tenantId] = new RabbitMqTenant(tenantId, virtualHostName);