File: AzureServiceBusExtensions.cs
Web Access
Project: src\src\Aspire.Hosting.Azure.ServiceBus\Aspire.Hosting.Azure.ServiceBus.csproj (Aspire.Hosting.Azure.ServiceBus)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
#pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
#pragma warning disable ASPIREPERSISTENCE001 // Resource lifetime APIs are experimental.
 
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using Aspire.Hosting;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure;
using Aspire.Hosting.Azure.ServiceBus;
using Azure.Provisioning;
using Azure.Provisioning.Expressions;
using Azure.Provisioning.ServiceBus;
using AzureProvisioning = Azure.Provisioning.ServiceBus;
 
namespace Aspire.Hosting;
 
/// <summary>
/// Provides extension methods for adding the Azure Service Bus resources to the application model.
/// </summary>
public static class AzureServiceBusExtensions
{
    private const string EmulatorHealthEndpointName = "emulatorhealth";
    private const string SqlServerEndpointName = "tcp";
    private const string SqlServerConflictMessage = "The Azure Service Bus emulator cannot use both an existing SQL Server resource and a customized built-in SQL Server resource. Remove either the 'WithSqlServer(sqlServer)' call or the 'WithSqlServer(configureSqlServer)' call.";
 
    /// <summary>
    /// Adds an Azure Service Bus Namespace resource to the application model. This resource can be used to create queue, topic, and subscription resources.
    /// </summary>
    /// <param name="builder">The builder for the distributed application.</param>
    /// <param name="name">The name of the resource.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    /// <ats-returns>The resource builder.</ats-returns>
    /// <remarks>
    /// By default references to the Azure Service Bus resource will be assigned the following roles:
    ///
    /// - <see cref="ServiceBusBuiltInRole.AzureServiceBusDataOwner"/>
    ///
    /// These can be replaced by calling <see cref="WithRoleAssignments{T}(IResourceBuilder{T}, IResourceBuilder{AzureServiceBusResource}, ServiceBusBuiltInRole[])"/>.
    /// </remarks>
    /// <ats-remarks />
    [AspireExport]
    public static IResourceBuilder<AzureServiceBusResource> AddAzureServiceBus(this IDistributedApplicationBuilder builder, [ResourceName] string name)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(name);
 
        builder.AddAzureProvisioning();
 
        var configureInfrastructure = static (AzureResourceInfrastructure infrastructure) =>
        {
            var azureResource = (AzureServiceBusResource)infrastructure.AspireResource;
 
            // Check if this Service Bus has a private endpoint (via annotation)
            var hasPrivateEndpoint = azureResource.HasAnnotationOfType<PrivateEndpointTargetAnnotation>();
 
            AzureProvisioning.ServiceBusNamespace serviceBusNamespace = AzureProvisioningResource.CreateExistingOrNewProvisionableResource(infrastructure,
                (identifier, name) =>
                {
                    var resource = AzureProvisioning.ServiceBusNamespace.FromExisting(identifier);
                    resource.Name = name;
                    return resource;
                },
                (infrastructure) =>
                {
                    var skuParameter = new ProvisioningParameter("sku", typeof(string))
                    {
                        Value = hasPrivateEndpoint ? "Premium" : "Standard"
                    };
                    infrastructure.Add(skuParameter);
                    var resource = new AzureProvisioning.ServiceBusNamespace(infrastructure.AspireResource.GetBicepIdentifier())
                    {
                        Sku = new AzureProvisioning.ServiceBusSku()
                        {
                            Name = skuParameter
                        },
                        DisableLocalAuth = true,
                        // When using private endpoints, disable public network access.
                        PublicNetworkAccess = hasPrivateEndpoint
                            ? AzureProvisioning.ServiceBusPublicNetworkAccess.Disabled
                            : AzureProvisioning.ServiceBusPublicNetworkAccess.Enabled,
                        Tags = { { "aspire-resource-name", infrastructure.AspireResource.Name } }
                    };
                    return resource;
                });
 
            infrastructure.Add(new ProvisioningOutput("serviceBusEndpoint", typeof(string)) { Value = serviceBusNamespace.ServiceBusEndpoint.ToBicepExpression() });
 
            // Extract hostname from endpoint: split(replace(endpoint, 'https://', ''), ':')[0]
            var replaceExpr = new FunctionCallExpression(
                new IdentifierExpression("replace"),
                serviceBusNamespace.ServiceBusEndpoint.Compile(),
                new StringLiteralExpression("https://"),
                new StringLiteralExpression(""));
            var splitExpr = new FunctionCallExpression(
                new IdentifierExpression("split"),
                replaceExpr,
                new StringLiteralExpression(":"));
            var hostNameExpr = new IndexExpression(splitExpr, new IntLiteralExpression(0));
 
            // We need the HostName specifically because the Azure SDK client use it instead of the full endpoint.
            infrastructure.Add(new ProvisioningOutput("serviceBusHostName", typeof(string)) { Value = (BicepValue<string>)hostNameExpr });
 
            // We need to output name to externalize role assignments.
            infrastructure.Add(new ProvisioningOutput("name", typeof(string)) { Value = serviceBusNamespace.Name.ToBicepExpression() });
 
            // Output the resource id for private endpoint support.
            infrastructure.Add(new ProvisioningOutput("id", typeof(string)) { Value = serviceBusNamespace.Id.ToBicepExpression() });
 
            foreach (var queue in azureResource.Queues)
            {
                var cdkQueue = queue.ToProvisioningEntity();
                cdkQueue.Parent = serviceBusNamespace;
                infrastructure.Add(cdkQueue);
            }
 
            foreach (var topic in azureResource.Topics)
            {
                var cdkTopic = topic.ToProvisioningEntity();
                cdkTopic.Parent = serviceBusNamespace;
                infrastructure.Add(cdkTopic);
 
                foreach (var subscription in topic.Subscriptions)
                {
                    var cdkSubscription = subscription.ToProvisioningEntity();
                    cdkSubscription.Parent = cdkTopic;
                    infrastructure.Add(cdkSubscription);
 
                    foreach (var rule in subscription.Rules)
                    {
                        var cdkRule = rule.ToProvisioningEntity();
                        cdkRule.Parent = cdkSubscription;
                        infrastructure.Add(cdkRule);
                    }
                }
            }
        };
 
        var resource = new AzureServiceBusResource(name, configureInfrastructure);
        return builder.AddResource(resource)
            .WithIconName("MailMultiple")
            .WithDefaultRoleAssignments(ServiceBusBuiltInRole.GetBuiltInRoleName,
                ServiceBusBuiltInRole.AzureServiceBusDataOwner);
    }
 
    /// <summary>
    /// Adds an Azure Service Bus Queue resource to the application model.
    /// </summary>
    /// <param name="builder">The Azure Service Bus resource builder.</param>
    /// <param name="name">The name of the queue resource.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    /// <remarks>This method is not available in polyglot app hosts. Use <see cref="AddServiceBusQueue"/> instead.</remarks>
    [AspireExportIgnore(Reason = "Obsolete API with incorrect return type. Use AddServiceBusQueue instead.")]
    [Obsolete($"This method is obsolete because it has the wrong return type and will be removed in a future version. Use {nameof(AddServiceBusQueue)} instead to add an Azure Service Bus Queue.")]
    public static IResourceBuilder<AzureServiceBusResource> AddQueue(this IResourceBuilder<AzureServiceBusResource> builder, [ResourceName] string name)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(name);
 
        builder.AddServiceBusQueue(name);
 
        return builder;
    }
 
    /// <summary>
    /// Adds an Azure Service Bus Queue resource to the application model.
    /// </summary>
    /// <param name="builder">The Azure Service Bus resource builder.</param>
    /// <param name="name">The name of the queue resource.</param>
    /// <param name="queueName">The name of the Service Bus Queue. If not provided, this defaults to the same value as <paramref name="name"/>.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    /// <ats-returns>The resource builder.</ats-returns>
    [AspireExport]
    public static IResourceBuilder<AzureServiceBusQueueResource> AddServiceBusQueue(this IResourceBuilder<AzureServiceBusResource> builder, [ResourceName] string name, string? queueName = null)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(name);
 
        // Use the resource name as the queue name if it's not provided
        queueName ??= name;
 
        var queue = new AzureServiceBusQueueResource(name, queueName, builder.Resource);
        builder.Resource.Queues.Add(queue);
 
        return builder.ApplicationBuilder.AddResource(queue)
            .WithIconName("Mail");
    }
 
    /// <summary>
    /// Allows setting the properties of an Azure Service Bus Queue resource.
    /// </summary>
    /// <param name="builder">The Azure Service Bus Queue resource builder.</param>
    /// <param name="configure">A method that can be used for customizing the <see cref="AzureServiceBusQueueResource"/>.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    /// <ats-returns>The resource builder.</ats-returns>
    [AspireExport("withQueueProperties", MethodName = "withProperties", RunSyncOnBackgroundThread = true)]
    public static IResourceBuilder<AzureServiceBusQueueResource> WithProperties(this IResourceBuilder<AzureServiceBusQueueResource> builder, Action<AzureServiceBusQueueResource> configure)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(configure);
 
        configure(builder.Resource);
 
        return builder;
    }
 
    /// <summary>
    /// Adds an Azure Service Bus Topic resource to the application model.
    /// </summary>
    /// <param name="builder">The Azure Service Bus resource builder.</param>
    /// <param name="name">The name of the topic resource.</param>
    /// <remarks>This method is not available in polyglot app hosts. Use <see cref="AddServiceBusTopic"/> instead.</remarks>
    [AspireExportIgnore(Reason = "Obsolete API with incorrect return type. Use AddServiceBusTopic instead.")]
    [Obsolete($"This method is obsolete because it has the wrong return type and will be removed in a future version. Use {nameof(AddServiceBusTopic)} instead to add an Azure Service Bus Topic.")]
    public static IResourceBuilder<AzureServiceBusResource> AddTopic(this IResourceBuilder<AzureServiceBusResource> builder, [ResourceName] string name)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(name);
 
        builder.AddServiceBusTopic(name);
 
        return builder;
    }
 
    /// <summary>
    /// Adds an Azure Service Bus Topic resource to the application model.
    /// </summary>
    /// <param name="builder">The Azure Service Bus resource builder.</param>
    /// <param name="name">The name of the topic resource.</param>
    /// <param name="subscriptions">The name of the subscriptions.</param>
    /// <remarks>This method is not available in polyglot app hosts. Use <see cref="AddServiceBusTopic"/> and <see cref="AddServiceBusSubscription"/> instead.</remarks>
    [AspireExportIgnore(Reason = "Obsolete API. Use AddServiceBusTopic and AddServiceBusSubscription instead.")]
    [Obsolete($"This method is obsolete because it has the wrong return type and will be removed in a future version. Use {nameof(AddServiceBusTopic)} and {nameof(AddServiceBusSubscription)} instead to add an Azure Service Bus Topic and Subscriptions.")]
    public static IResourceBuilder<AzureServiceBusResource> AddTopic(this IResourceBuilder<AzureServiceBusResource> builder, [ResourceName] string name, string[] subscriptions)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(name);
        ArgumentNullException.ThrowIfNull(subscriptions);
 
        var topic = builder.AddServiceBusTopic(name);
 
        foreach (var subscription in subscriptions)
        {
            ArgumentException.ThrowIfNullOrEmpty(subscription);
            topic.AddServiceBusSubscription(subscription);
        }
 
        return builder;
    }
 
    /// <summary>
    /// Adds an Azure Service Bus Topic resource to the application model.
    /// </summary>
    /// <param name="builder">The Azure Service Bus resource builder.</param>
    /// <param name="name">The name of the topic resource.</param>
    /// <param name="topicName">The name of the Service Bus Topic. If not provided, this defaults to the same value as <paramref name="name"/>.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    /// <ats-returns>The resource builder.</ats-returns>
    [AspireExport]
    public static IResourceBuilder<AzureServiceBusTopicResource> AddServiceBusTopic(this IResourceBuilder<AzureServiceBusResource> builder, [ResourceName] string name, string? topicName = null)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(name);
 
        // Use the resource name as the topic name if it's not provided
        topicName ??= name;
 
        var topic = new AzureServiceBusTopicResource(name, topicName, builder.Resource);
        builder.Resource.Topics.Add(topic);
 
        return builder.ApplicationBuilder.AddResource(topic)
            .WithIconName("Mail");
    }
 
    /// <summary>
    /// Allows setting the properties of an Azure Service Bus Topic resource.
    /// </summary>
    /// <param name="builder">The Azure Service Bus Topic resource builder.</param>
    /// <param name="configure">A method that can be used for customizing the <see cref="AzureServiceBusTopicResource"/>.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    /// <ats-returns>The resource builder.</ats-returns>
    [AspireExport("withTopicProperties", MethodName = "withProperties", RunSyncOnBackgroundThread = true)]
    public static IResourceBuilder<AzureServiceBusTopicResource> WithProperties(this IResourceBuilder<AzureServiceBusTopicResource> builder, Action<AzureServiceBusTopicResource> configure)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(configure);
 
        configure(builder.Resource);
 
        return builder;
    }
 
    /// <summary>
    /// Adds an Azure Service Bus Subscription resource to the application model.
    /// </summary>
    /// <param name="builder">The Azure Service Bus resource builder.</param>
    /// <param name="topicName">The name of the topic resource.</param>
    /// <param name="subscriptionName">The name of the subscription.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    /// <remarks>This method is not available in polyglot app hosts. Use <see cref="AddServiceBusSubscription"/> instead.</remarks>
    [AspireExportIgnore(Reason = "Obsolete API. Use AddServiceBusSubscription instead.")]
    [Obsolete($"This method is obsolete and will be removed in a future version. Use {nameof(AddServiceBusSubscription)} instead to add an Azure Service Bus Subscription to a Topic.")]
    public static IResourceBuilder<AzureServiceBusResource> AddSubscription(this IResourceBuilder<AzureServiceBusResource> builder, string topicName, string subscriptionName)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(topicName);
        ArgumentException.ThrowIfNullOrEmpty(subscriptionName);
 
        IResourceBuilder<AzureServiceBusTopicResource> topicBuilder;
        if (builder.Resource.Topics.FirstOrDefault(x => x.Name == topicName) is { } existingResource)
        {
            topicBuilder = builder.ApplicationBuilder.CreateResourceBuilder(existingResource);
        }
        else
        {
            topicBuilder = builder.AddServiceBusTopic(topicName);
        }
 
        topicBuilder.AddServiceBusSubscription(subscriptionName);
 
        return builder;
    }
 
    /// <summary>
    /// Adds an Azure Service Bus Subscription resource to the application model.
    /// </summary>
    /// <param name="builder">The Azure Service Bus Topic resource builder.</param>
    /// <param name="name">The name of the subscription resource.</param>
    /// <param name="subscriptionName">The name of the Service Bus Subscription. If not provided, this defaults to the same value as <paramref name="name"/>.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    /// <ats-returns>The resource builder.</ats-returns>
    [AspireExport]
    public static IResourceBuilder<AzureServiceBusSubscriptionResource> AddServiceBusSubscription(this IResourceBuilder<AzureServiceBusTopicResource> builder, [ResourceName] string name, string? subscriptionName = null)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(name);
 
        // Use the resource name as the subscription name if it's not provided
        subscriptionName ??= name;
 
        var subscription = new AzureServiceBusSubscriptionResource(name, subscriptionName, builder.Resource);
        builder.Resource.Subscriptions.Add(subscription);
 
        return builder.ApplicationBuilder.AddResource(subscription)
            .WithIconName("Mail");
    }
 
    /// <summary>
    /// Allows setting the properties of an Azure Service Bus Subscription resource.
    /// </summary>
    /// <param name="builder">The Azure Service Bus Subscription resource builder.</param>
    /// <param name="configure">A method that can be used for customizing the <see cref="AzureServiceBusSubscriptionResource"/>.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    /// <ats-returns>The resource builder.</ats-returns>
    [AspireExport("withSubscriptionProperties", MethodName = "withProperties", RunSyncOnBackgroundThread = true)]
    public static IResourceBuilder<AzureServiceBusSubscriptionResource> WithProperties(this IResourceBuilder<AzureServiceBusSubscriptionResource> builder, Action<AzureServiceBusSubscriptionResource> configure)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(configure);
 
        configure(builder.Resource);
 
        return builder;
    }
 
    /// <summary>
    /// Configures an Azure Service Bus resource to be emulated. This resource requires an <see cref="AzureServiceBusResource"/> to be added to the application model.
    /// </summary>
    /// <ats-summary>Configures the Azure Service Bus resource to run with the local emulator</ats-summary>
    /// <remarks>
    /// This version of the package defaults to the <inheritdoc cref="ServiceBusEmulatorContainerImageTags.Tag"/> tag of the <inheritdoc cref="ServiceBusEmulatorContainerImageTags.Registry"/>/<inheritdoc cref="ServiceBusEmulatorContainerImageTags.Image"/> container image.
    /// <param name="builder">The Azure Service Bus resource builder.</param>
    /// <param name="configureContainer">Callback that exposes underlying container used for emulation to allow for customization.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    /// <ats-returns>The resource builder.</ats-returns>
    /// <example>
    /// The following example creates an Azure Service Bus resource that runs locally is an emulator and referencing that
    /// resource in a .NET project.
    /// <code lang="csharp">
    /// var builder = DistributedApplication.CreateBuilder(args);
    ///
    /// var serviceBus = builder.AddAzureServiceBus("myservicebus")
    ///    .RunAsEmulator()
    ///    .AddQueue("queue");
    ///
    /// builder.AddProject&lt;Projects.InventoryService&gt;()
    ///        .WithReference(serviceBus);
    ///
    /// builder.Build().Run();
    /// </code>
    /// </example>
    /// </remarks>
    /// <ats-remarks />
    [AspireExport(RunSyncOnBackgroundThread = true)]
    public static IResourceBuilder<AzureServiceBusResource> RunAsEmulator(this IResourceBuilder<AzureServiceBusResource> builder, Action<IResourceBuilder<AzureServiceBusEmulatorResource>>? configureContainer = null)
    {
        ArgumentNullException.ThrowIfNull(builder);
 
        if (builder.Resource.IsEmulator)
        {
            throw new InvalidOperationException("The Azure Service Bus resource is already configured to run as an emulator.");
        }
 
        if (builder.ApplicationBuilder.ExecutionContext.IsPublishMode)
        {
            return builder;
        }
 
        // Mark this resource as an emulator for consistent resource identification and tooling support
        builder.WithAnnotation(new EmulatorResourceAnnotation());
 
        // Add emulator container
 
        builder
            .WithEndpoint(name: "emulator", targetPort: 5672)
            .WithHttpEndpoint(name: EmulatorHealthEndpointName, targetPort: 5300)
            .WithEndpoint(EmulatorHealthEndpointName, e => e.ExcludeReferenceEndpoint = true)
            .WithAnnotation(new ContainerImageAnnotation
            {
                Registry = ServiceBusEmulatorContainerImageTags.Registry,
                Image = ServiceBusEmulatorContainerImageTags.Image,
                Tag = ServiceBusEmulatorContainerImageTags.Tag
            })
            .WithUrlForEndpoint(EmulatorHealthEndpointName, u => u.DisplayLocation = UrlDisplayLocation.DetailsOnly);
 
        var surrogate = new AzureServiceBusEmulatorResource(builder.Resource);
        var surrogateBuilder = builder.ApplicationBuilder.CreateResourceBuilder(surrogate);
 
        configureContainer?.Invoke(surrogateBuilder);
 
        // The emulator needs a SQL Server instance to store its state. By default a dedicated SQL Server
        // resource is created, but WithSqlServer can be used to point the emulator at an existing SQL Server
        // resource, in which case a SqlServerConnectionAnnotation is already present. The two options are
        // mutually exclusive, which the extension methods enforce. Annotations added through the surrogate
        // land on the inner resource (AzureServiceBusEmulatorResource forwards its Annotations collection),
        // so they are visible on builder.Resource here.
        if (!builder.Resource.HasAnnotationOfType<SqlServerConnectionAnnotation>())
        {
            // Preserve the password parameter name used before the SQL Server integration owned this resource.
            // Existing configuration and persisted emulator stores can depend on that stable parameter name.
            var password = ParameterResourceBuilderExtensions.CreateDefaultPasswordParameter(
                builder.ApplicationBuilder,
                $"{builder.Resource.Name}-sql-pwd",
                minLower: 1,
                minUpper: 1,
                minNumeric: 1);
            var passwordBuilder = builder.ApplicationBuilder.CreateResourceBuilder(password);
 
            var sqlServerBuilder = builder.ApplicationBuilder
                .AddSqlServer($"{builder.Resource.Name}-mssql", passwordBuilder)
                .WithParentRelationship(builder);
 
            if (configureContainer != null)
            {
                sqlServerBuilder = sqlServerBuilder.WithLifetimeOf(surrogateBuilder);
            }
 
            // Apply the WithSqlServer customizations last so they take precedence over the defaults.
            foreach (var sqlServerConfiguration in builder.Resource.Annotations.OfType<SqlServerConfigurationAnnotation>())
            {
                sqlServerConfiguration.Configure(sqlServerBuilder);
            }
 
            if (!sqlServerBuilder.Resource.Annotations.OfType<EndpointAnnotation>().Any(e => e.Name == SqlServerEndpointName))
            {
                throw new InvalidOperationException($"The SQL Server resource for the Azure Service Bus emulator must keep its '{SqlServerEndpointName}' endpoint. Update the 'WithSqlServer' callback so it does not remove or rename the endpoint.");
            }
 
            // The emulator fails to start when its backing store isn't accepting connections yet.
            surrogateBuilder.WaitFor(sqlServerBuilder);
 
            builder.WithAnnotation(new SqlServerConnectionAnnotation(sqlServerBuilder.Resource, []));
        }
 
        // The environment callback is registered after the SQL Server connection has been resolved so a failed
        // configuration callback doesn't leak a callback into the model. The values are set as defaults (only
        // when not already present) so user-provided overrides always win, regardless of the order in which the
        // environment callbacks run.
        builder.WithAnnotation(new EnvironmentCallbackAnnotation((EnvironmentCallbackContext context) =>
        {
            if (!builder.Resource.TryGetLastAnnotation<SqlServerConnectionAnnotation>(out var sqlConnection))
            {
                throw new InvalidOperationException("The SQL Server instance for the Azure Service Bus emulator was not configured. This indicates that 'RunAsEmulator' did not complete successfully.");
            }
 
            context.EnvironmentVariables.TryAdd("ACCEPT_EULA", "Y");
            // The endpoint is resolved in the emulator container's network context, so the SQL Server
            // resolves to its container-network address rather than to an address that is only reachable
            // from the host.
            context.EnvironmentVariables.TryAdd("SQL_SERVER", sqlConnection.Endpoint.Property(EndpointProperty.HostAndPort));
            context.EnvironmentVariables.TryAdd("MSSQL_SA_PASSWORD", sqlConnection.Password);
        }));
 
        // RunAsEmulator() can be followed by custom model configuration so we need to delay the creation of the Config.json file
        // until all resources are about to be prepared and annotations can't be updated anymore.
        surrogateBuilder.WithContainerFiles(
            AzureServiceBusEmulatorResource.EmulatorConfigFilesPath,
            (_, _) =>
            {
                var customConfigFile = builder.Resource.Annotations.OfType<ConfigFileAnnotation>().FirstOrDefault();
                if (customConfigFile != null)
                {
                    return Task.FromResult<IEnumerable<ContainerFileSystemItem>>([
                        new ContainerFile
                        {
                            Name = AzureServiceBusEmulatorResource.EmulatorConfigJsonFile,
                            SourcePath = customConfigFile.SourcePath,
                        },
                    ]);
                }
 
                // Create default Config.json file content
                var tempConfig = JsonNode.Parse(CreateEmulatorConfigJson(builder.Resource));
 
                if (tempConfig == null)
                {
                    throw new InvalidOperationException("The configuration file mount could not be parsed.");
                }
 
                // Apply ConfigJsonAnnotation modifications
                var configJsonAnnotations = builder.Resource.Annotations.OfType<ConfigJsonAnnotation>();
 
                if (configJsonAnnotations.Any())
                {
                    foreach (var annotation in configJsonAnnotations)
                    {
                        annotation.Configure(tempConfig);
                    }
                }
 
                using var writeStream = new MemoryStream();
                using var writer = new Utf8JsonWriter(writeStream, new JsonWriterOptions { Indented = true });
                tempConfig.WriteTo(writer);
 
                writer.Flush();
 
                return Task.FromResult<IEnumerable<ContainerFileSystemItem>>([
                    new ContainerFile
                    {
                        Name = AzureServiceBusEmulatorResource.EmulatorConfigJsonFile,
                        Contents = Encoding.UTF8.GetString(writeStream.ToArray()),
                    },
                ]);
            }
        );
 
        builder.WithHttpHealthCheck(endpointName: EmulatorHealthEndpointName, path: "/health");
 
        return builder;
    }
 
    /// <summary>
    /// Copies the configuration file into an Azure Service Bus emulator resource.
    /// </summary>
    /// <param name="builder">The builder for the <see cref="AzureServiceBusEmulatorResource"/>.</param>
    /// <param name="path">Path to the file on the AppHost where the emulator configuration is located.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    /// <ats-returns>The resource builder.</ats-returns>
    [AspireExport]
    public static IResourceBuilder<AzureServiceBusEmulatorResource> WithConfigurationFile(this IResourceBuilder<AzureServiceBusEmulatorResource> builder, string path)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(path);
 
        return builder.WithAnnotation(new ConfigFileAnnotation(path), ResourceAnnotationMutationBehavior.Replace);
    }
 
    /// <summary>
    /// Alters the JSON configuration document used by the emulator.
    /// </summary>
    /// <param name="builder">The builder for the <see cref="AzureServiceBusEmulatorResource"/>.</param>
    /// <param name="configJson">A callback to update the JSON object representation of the configuration.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    /// <remarks>
    /// This method is not available in polyglot app hosts. Use <see cref="WithConfigurationFile"/> instead to provide a configuration file.
    /// <example>
    /// Here is an example of how to configure the emulator to use a different logging mechanism:
    /// <code language="csharp">
    /// var builder = DistributedApplication.CreateBuilder(args);
    ///
    /// builder.AddAzureServiceBus("servicebusns")
    ///        .RunAsEmulator(configure => configure
    ///            .WithConfiguration(document =>
    ///            {
    ///                document["UserConfig"]!["Logging"] = new JsonObject { ["Type"] = "Console" };
    ///            });
    ///        );
    /// </code>
    /// </example>
    /// </remarks>
    [AspireExportIgnore(Reason = "Action<JsonNode> callbacks are not ATS-compatible.")]
    public static IResourceBuilder<AzureServiceBusEmulatorResource> WithConfiguration(this IResourceBuilder<AzureServiceBusEmulatorResource> builder, Action<JsonNode> configJson)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(configJson);
 
        builder.WithAnnotation(new ConfigJsonAnnotation(configJson));
 
        return builder;
    }
 
    /// <summary>
    /// Configures the host port for the Azure Service Bus emulator is exposed on instead of using randomly assigned port.
    /// </summary>
    /// <param name="builder">Builder for the Azure Service Bus emulator container</param>
    /// <param name="port">The port to bind on the host. If <see langword="null"/> is used, a random port will be assigned.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    /// <ats-returns>The resource builder.</ats-returns>
    [AspireExport]
    public static IResourceBuilder<AzureServiceBusEmulatorResource> WithHostPort(this IResourceBuilder<AzureServiceBusEmulatorResource> builder, int? port)
    {
        ArgumentNullException.ThrowIfNull(builder);
 
        return builder.WithEndpoint("emulator", endpoint =>
        {
            endpoint.Port = port;
        });
    }
 
    /// <summary>
    /// Configures the Azure Service Bus emulator to store its state in the SQL Server resource that is
    /// created for it.
    /// </summary>
    /// <param name="builder">Builder for the Azure Service Bus emulator container</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    /// <remarks>
    /// This is the default behavior, so this method only needs to be called to state that intent explicitly.
    /// It cannot be combined with <see cref="WithSqlServer(IResourceBuilder{AzureServiceBusEmulatorResource}, IResourceBuilder{SqlServerServerResource})"/>.
    /// </remarks>
    [AspireExportIgnore(Reason = "The emulator creates its own SQL Server resource by default, so there is nothing to configure. Use the WithSqlServer overload that takes a SQL Server resource to reuse an existing one.")]
    public static IResourceBuilder<AzureServiceBusEmulatorResource> WithSqlServer(this IResourceBuilder<AzureServiceBusEmulatorResource> builder)
    {
        ArgumentNullException.ThrowIfNull(builder);
 
        return builder.WithSqlServer(static _ => { });
    }
 
    /// <summary>
    /// Customizes the SQL Server resource that backs the Azure Service Bus emulator, allowing the regular
    /// SQL Server integration APIs (such as data volumes, host ports or the password) to be used.
    /// </summary>
    /// <param name="builder">Builder for the Azure Service Bus emulator container</param>
    /// <param name="configureSqlServer">Callback that exposes the SQL Server resource used by the emulator to allow for customization.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    /// <remarks>
    /// This method can be called multiple times; the callbacks are applied in order. It cannot be combined
    /// with <see cref="WithSqlServer(IResourceBuilder{AzureServiceBusEmulatorResource}, IResourceBuilder{SqlServerServerResource})"/>.
    /// The emulator connects to the SQL Server resource through its <c>tcp</c> endpoint, which must not be
    /// removed or renamed by the callback.
    /// <example>
    /// Here is an example of how to persist the emulator state in a data volume and use a specific container name:
    /// <code language="csharp">
    /// var builder = DistributedApplication.CreateBuilder(args);
    ///
    /// builder.AddAzureServiceBus("servicebusns")
    ///        .RunAsEmulator(configure => configure
    ///            .WithSqlServer(sql => sql
    ///                .WithDataVolume()
    ///                .WithContainerName("myproject-servicebus-sql")));
    /// </code>
    /// </example>
    /// </remarks>
    [AspireExportIgnore(Reason = "Action<IResourceBuilder<SqlServerServerResource>> callbacks are not ATS-compatible. Use the WithSqlServer overload that takes a SQL Server resource instead.")]
    public static IResourceBuilder<AzureServiceBusEmulatorResource> WithSqlServer(this IResourceBuilder<AzureServiceBusEmulatorResource> builder, Action<IResourceBuilder<SqlServerServerResource>> configureSqlServer)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(configureSqlServer);
 
        if (builder.Resource.HasAnnotationOfType<SqlServerConnectionAnnotation>())
        {
            throw new InvalidOperationException(SqlServerConflictMessage);
        }
 
        return builder.WithAnnotation(new SqlServerConfigurationAnnotation(configureSqlServer));
    }
 
    /// <summary>
    /// Configures the Azure Service Bus emulator to use an existing SQL Server resource instead of
    /// creating one for it.
    /// </summary>
    /// <param name="builder">Builder for the Azure Service Bus emulator container</param>
    /// <param name="sqlServer">The SQL Server resource the emulator should use to store its state.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/>.</returns>
    /// <ats-returns>The resource builder.</ats-returns>
    /// <remarks>
    /// The emulator waits for the SQL Server resource to become healthy and connects to it over the container
    /// network using its primary endpoint and administrator password. If this method is called multiple times
    /// the last call wins. It cannot be combined with
    /// <see cref="WithSqlServer(IResourceBuilder{AzureServiceBusEmulatorResource}, Action{IResourceBuilder{SqlServerServerResource}})"/>.
    /// <example>
    /// Here is an example of how to share a SQL Server resource with the emulator:
    /// <code language="csharp">
    /// var builder = DistributedApplication.CreateBuilder(args);
    ///
    /// var sql = builder.AddSqlServer("sql");
    ///
    /// builder.AddAzureServiceBus("servicebusns")
    ///        .RunAsEmulator(configure => configure
    ///            .WithSqlServer(sql));
    /// </code>
    /// </example>
    /// </remarks>
    /// <ats-remarks />
    [AspireExport]
    public static IResourceBuilder<AzureServiceBusEmulatorResource> WithSqlServer(this IResourceBuilder<AzureServiceBusEmulatorResource> builder, IResourceBuilder<SqlServerServerResource> sqlServer)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(sqlServer);
 
        if (builder.Resource.HasAnnotationOfType<SqlServerConfigurationAnnotation>())
        {
            throw new InvalidOperationException(SqlServerConflictMessage);
        }
 
        // A previous call is replaced entirely, so the emulator doesn't keep waiting for a SQL Server
        // resource it no longer uses.
        if (builder.Resource.TryGetLastAnnotation<SqlServerConnectionAnnotation>(out var previousConnection))
        {
            RemoveWaitFor(builder.Resource, previousConnection.WaitAnnotations);
        }
 
        // The emulator fails to start when its backing store isn't accepting connections yet.
        var existingAnnotations = builder.Resource.Annotations.ToHashSet();
        builder.WaitFor(sqlServer);
        var waitAnnotations = builder.Resource.Annotations
            .Where(annotation => !existingAnnotations.Contains(annotation))
            .ToArray();
 
        return builder.WithAnnotation(new SqlServerConnectionAnnotation(sqlServer.Resource, waitAnnotations), ResourceAnnotationMutationBehavior.Replace);
    }
 
    private static void RemoveWaitFor(IResource resource, IReadOnlyList<IResourceAnnotation> waitAnnotations)
    {
        foreach (var waitAnnotation in waitAnnotations)
        {
            resource.Annotations.Remove(waitAnnotation);
        }
    }
 
    private static string CreateEmulatorConfigJson(AzureServiceBusResource emulatorResource)
    {
        using var stream = new MemoryStream();
        using var writer = new Utf8JsonWriter(stream, new JsonWriterOptions { Indented = true });
 
        writer.WriteStartObject();                      // {
        writer.WriteStartObject("UserConfig");          //   "UserConfig": {
        writer.WriteStartArray("Namespaces");           //     "Namespaces": [
        writer.WriteStartObject();                      //       {
        writer.WriteString("Name", emulatorResource.Name);
        writer.WriteStartArray("Queues");               //         "Queues": [
 
        foreach (var queue in emulatorResource.Queues)
        {
            writer.WriteStartObject();
            queue.WriteJsonObjectProperties(writer);
            writer.WriteEndObject();
        }
 
        writer.WriteEndArray();                         //         ] (/Queues)
 
        writer.WriteStartArray("Topics");               //         "Topics": [
        foreach (var topic in emulatorResource.Topics)
        {
            writer.WriteStartObject();                  //           "{ (Topic)"
            topic.WriteJsonObjectProperties(writer);
 
            writer.WriteStartArray("Subscriptions");    //             "Subscriptions": [
            foreach (var subscription in topic.Subscriptions)
            {
                writer.WriteStartObject();              //               "{ (Subscription)"
                subscription.WriteJsonObjectProperties(writer);
 
                writer.WriteStartArray("Rules");        //                 "Rules": [
                foreach (var rule in subscription.Rules)
                {
                    writer.WriteStartObject();
                    rule.WriteJsonObjectProperties(writer);
                    writer.WriteEndObject();
                }
 
                writer.WriteEndArray();                 //                  ] (/Rules)
 
                writer.WriteEndObject();                //               } (/Subscription)
            }
 
            writer.WriteEndArray();                     //             ] (/Subscriptions)
 
            writer.WriteEndObject();                    //           } (/Topic)
        }
        writer.WriteEndArray();                         //         ] (/Topics)
 
        writer.WriteEndObject();                        //       } (/Namespace)
        writer.WriteEndArray();                         //     ], (/Namespaces)
        writer.WriteStartObject("Logging");             //     "Logging": {
        writer.WriteString("Type", "File");             //       "Type": "File"
        writer.WriteEndObject();                        //     } (/LoggingConfig)
 
        writer.WriteEndObject();                        //   } (/UserConfig)
        writer.WriteEndObject();                        // } (/Root)
 
        writer.Flush();
 
        return Encoding.UTF8.GetString(stream.ToArray());
    }
 
    /// <summary>
    /// Assigns the specified roles to the given resource, granting it the necessary permissions
    /// on the target Azure Service Bus namespace. This replaces the default role assignments for the resource.
    /// </summary>
    /// <param name="builder">The resource to which the specified roles will be assigned.</param>
    /// <param name="target">The target Azure Service Bus namespace.</param>
    /// <param name="roles">The built-in Service Bus roles to be assigned.</param>
    /// <returns>The updated <see cref="IResourceBuilder{T}"/> with the applied role assignments.</returns>
    /// <remarks>
    /// This overload is not available in polyglot app hosts. Use
    /// <see cref="WithRoleAssignments{T}(IResourceBuilder{T}, IResourceBuilder{AzureServiceBusResource}, AzureServiceBusRole[])"/>
    /// instead.
    /// <example>
    /// <code lang="csharp">
    /// var builder = DistributedApplication.CreateBuilder(args);
    ///
    /// var sb = builder.AddAzureServiceBus("bus");
    ///
    /// var api = builder.AddProject&lt;Projects.Api&gt;("api")
    ///   .WithRoleAssignments(sb, ServiceBusBuiltInRole.AzureServiceBusDataSender)
    ///   .WithReference(sb);
    /// </code>
    /// </example>
    /// </remarks>
    [AspireExportIgnore(Reason = "ServiceBusBuiltInRole is an Azure.Provisioning type not compatible with ATS. Use the AzureServiceBusRole-based overload instead.")]
    public static IResourceBuilder<T> WithRoleAssignments<T>(
        this IResourceBuilder<T> builder,
        IResourceBuilder<AzureServiceBusResource> target,
        params ServiceBusBuiltInRole[] roles)
        where T : IResource
    {
        return builder.WithRoleAssignments(target, ServiceBusBuiltInRole.GetBuiltInRoleName, roles);
    }
 
    /// <summary>
    /// Assigns the specified roles to the given resource, granting it the necessary permissions
    /// on the target Azure Service Bus namespace. This replaces the default role assignments for the resource.
    /// </summary>
    /// <param name="builder">The resource to which the specified roles will be assigned.</param>
    /// <param name="target">The target Azure Service Bus namespace.</param>
    /// <param name="roles">The Service Bus roles to be assigned.</param>
    /// <returns>The updated <see cref="IResourceBuilder{T}"/> with the applied role assignments.</returns>
    /// <ats-returns>The resource builder.</ats-returns>
    /// <exception cref="ArgumentException">Thrown when a role value is not a valid <see cref="AzureServiceBusRole"/> value.</exception>
    [AspireExport("withServiceBusRoleAssignments")]
    internal static IResourceBuilder<T> WithRoleAssignments<T>(
        this IResourceBuilder<T> builder,
        IResourceBuilder<AzureServiceBusResource> target,
        params AzureServiceBusRole[] roles)
        where T : IResource
    {
        if (roles is null || roles.Length == 0)
        {
            return builder.WithRoleAssignments(target, Array.Empty<ServiceBusBuiltInRole>());
        }
 
        var builtInRoles = new ServiceBusBuiltInRole[roles.Length];
        for (var i = 0; i < roles.Length; i++)
        {
            builtInRoles[i] = roles[i] switch
            {
                AzureServiceBusRole.AzureServiceBusDataOwner => ServiceBusBuiltInRole.AzureServiceBusDataOwner,
                AzureServiceBusRole.AzureServiceBusDataReceiver => ServiceBusBuiltInRole.AzureServiceBusDataReceiver,
                AzureServiceBusRole.AzureServiceBusDataSender => ServiceBusBuiltInRole.AzureServiceBusDataSender,
                _ => throw new ArgumentException($"'{roles[i]}' is not a valid {nameof(AzureServiceBusRole)} value.", nameof(roles))
            };
        }
 
        return builder.WithRoleAssignments(target, builtInRoles);
    }
}