File: Provisioning\Internal\PublishModeProvisioningContextProvider.cs
Web Access
Project: src\src\Aspire.Hosting.Azure\Aspire.Hosting.Azure.csproj (Aspire.Hosting.Azure)
#pragma warning disable ASPIREINTERACTION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
 
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Reflection;
using System.Text.Json.Nodes;
using Aspire.Hosting.Azure.Resources;
using Aspire.Hosting.Azure.Utils;
using Azure.Core;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
 
namespace Aspire.Hosting.Azure.Provisioning.Internal;
 
/// <summary>
/// Publish mode implementation of <see cref="IProvisioningContextProvider"/>.
/// Uses enhanced prompting logic with dynamic subscription and location fetching.
/// </summary>
internal sealed class PublishModeProvisioningContextProvider(
    IInteractionService interactionService,
    IOptions<AzureProvisionerOptions> options,
    IHostEnvironment environment,
    ILogger<PublishModeProvisioningContextProvider> logger,
    IArmClientProvider armClientProvider,
    IUserPrincipalProvider userPrincipalProvider,
    ITokenCredentialProvider tokenCredentialProvider,
    DistributedApplicationExecutionContext distributedApplicationExecutionContext) : BaseProvisioningContextProvider(
        interactionService,
        options,
        environment,
        logger,
        armClientProvider,
        userPrincipalProvider,
        tokenCredentialProvider,
        distributedApplicationExecutionContext)
{
    protected override string GetDefaultResourceGroupName()
    {
        var prefix = "rg-aspire";
 
        if (!string.IsNullOrWhiteSpace(_options.ResourceGroupPrefix))
        {
            prefix = _options.ResourceGroupPrefix;
        }
 
        var maxApplicationNameSize = ResourceGroupNameHelpers.MaxResourceGroupNameLength - prefix.Length - 1; // extra '-'
 
        var normalizedApplicationName = ResourceGroupNameHelpers.NormalizeResourceGroupName(_environment.ApplicationName.ToLowerInvariant());
        if (normalizedApplicationName.Length > maxApplicationNameSize)
        {
            normalizedApplicationName = normalizedApplicationName[..maxApplicationNameSize];
        }
 
        // Publish mode doesn't include random suffix for consistency
        return $"{prefix}-{normalizedApplicationName}";
    }
 
    public override async Task<ProvisioningContext> CreateProvisioningContextAsync(JsonObject userSecrets, CancellationToken cancellationToken = default)
    {
        try
        {
            await RetrieveAzureProvisioningOptions(cancellationToken).ConfigureAwait(false);
            _logger.LogDebug("Azure provisioning options have been handled successfully.");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to retrieve Azure provisioning options.");
        }
 
        return await base.CreateProvisioningContextAsync(userSecrets, cancellationToken).ConfigureAwait(false);
    }
 
    private async Task RetrieveAzureProvisioningOptions(CancellationToken cancellationToken = default)
    {
        while (_options.Location == null || _options.SubscriptionId == null)
        {
            if (_options.SubscriptionId == null)
            {
                await PromptForSubscriptionAsync(cancellationToken).ConfigureAwait(false);
                if (_options.SubscriptionId == null)
                {
                    continue;
                }
            }
 
            if (_options.Location == null)
            {
                await PromptForLocationAndResourceGroupAsync(cancellationToken).ConfigureAwait(false);
                if (_options.Location == null)
                {
                    continue;
                }
            }
        }
    }
 
    private async Task PromptForSubscriptionAsync(CancellationToken cancellationToken)
    {
        try
        {
            // Try to enumerate available subscriptions using Azure APIs
            var credential = _tokenCredentialProvider.TokenCredential;
            var armClient = _armClientProvider.GetArmClient(credential);
            var availableSubscriptions = await armClient.GetAvailableSubscriptionsAsync(cancellationToken).ConfigureAwait(false);
            var subscriptionList = availableSubscriptions.ToList();
 
            if (subscriptionList.Count > 0)
            {
                // Present subscriptions as a choice list
                var subscriptionOptions = subscriptionList
                    .Select(sub => KeyValuePair.Create(sub.Id.SubscriptionId ?? "", $"{sub.DisplayName ?? sub.Id.SubscriptionId} ({sub.Id.SubscriptionId})"))
                    .OrderBy(kvp => kvp.Value)
                    .ToList();
 
                var result = await _interactionService.PromptInputsAsync(
                    AzureProvisioningStrings.SubscriptionDialogTitle,
                    AzureProvisioningStrings.SubscriptionSelectionMessage,
                    [
                        new InteractionInput
                        {
                            Name = SubscriptionIdName,
                            InputType = InputType.Choice,
                            Label = AzureProvisioningStrings.SubscriptionIdLabel,
                            Required = true,
                            Options = [..subscriptionOptions]
                        }
                    ],
                    new InputsDialogInteractionOptions
                    {
                        EnableMessageMarkdown = false
                    },
                    cancellationToken).ConfigureAwait(false);
 
                if (!result.Canceled)
                {
                    _options.SubscriptionId = result.Data[SubscriptionIdName].Value;
                }
                return;
            }
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to enumerate available subscriptions. Falling back to manual input.");
        }
 
        // Fallback to manual subscription entry
        var manualResult = await _interactionService.PromptInputsAsync(
            AzureProvisioningStrings.SubscriptionDialogTitle,
            AzureProvisioningStrings.SubscriptionManualEntryMessage,
            [
                new InteractionInput
                {
                    Name = SubscriptionIdName,
                    InputType = InputType.SecretText,
                    Label = AzureProvisioningStrings.SubscriptionIdLabel,
                    Placeholder = AzureProvisioningStrings.SubscriptionIdPlaceholder,
                    Required = true
                }
            ],
            new InputsDialogInteractionOptions
            {
                EnableMessageMarkdown = false,
                ValidationCallback = static (validationContext) =>
                {
                    var subscriptionInput = validationContext.Inputs[SubscriptionIdName];
                    if (!Guid.TryParse(subscriptionInput.Value, out var _))
                    {
                        validationContext.AddValidationError(subscriptionInput, AzureProvisioningStrings.ValidationSubscriptionIdInvalid);
                    }
                    return Task.CompletedTask;
                }
            },
            cancellationToken).ConfigureAwait(false);
 
        if (!manualResult.Canceled)
        {
            _options.SubscriptionId = manualResult.Data[SubscriptionIdName].Value;
        }
    }
 
    private async Task PromptForLocationAndResourceGroupAsync(CancellationToken cancellationToken)
    {
        try
        {
            // Try to enumerate available locations using Azure APIs
            var credential = _tokenCredentialProvider.TokenCredential;
            var armClient = _armClientProvider.GetArmClient(credential);
            var availableLocations = await armClient.GetAvailableLocationsAsync(_options.SubscriptionId!, cancellationToken).ConfigureAwait(false);
            var locationList = availableLocations.ToList();
 
            if (locationList.Count > 0)
            {
                // Present locations as a choice list
                var locationOptions = locationList
                    .Select(loc => KeyValuePair.Create(loc.Name, loc.DisplayName))
                    .ToList();
 
                var result = await _interactionService.PromptInputsAsync(
                    AzureProvisioningStrings.LocationDialogTitle,
                    AzureProvisioningStrings.LocationSelectionMessage,
                    [
                        new InteractionInput
                        {
                            Name = LocationName,
                            InputType = InputType.Choice,
                            Label = AzureProvisioningStrings.LocationLabel,
                            Required = true,
                            Options = [..locationOptions]
                        },
                        new InteractionInput
                        {
                            Name = ResourceGroupName,
                            InputType = InputType.Text,
                            Label = AzureProvisioningStrings.ResourceGroupLabel,
                            Value = GetDefaultResourceGroupName()
                        }
                    ],
                    new InputsDialogInteractionOptions
                    {
                        EnableMessageMarkdown = false,
                        ValidationCallback = static (validationContext) =>
                        {
                            var resourceGroupInput = validationContext.Inputs[ResourceGroupName];
                            if (!IsValidResourceGroupName(resourceGroupInput.Value))
                            {
                                validationContext.AddValidationError(resourceGroupInput, AzureProvisioningStrings.ValidationResourceGroupNameInvalid);
                            }
                            return Task.CompletedTask;
                        }
                    },
                    cancellationToken).ConfigureAwait(false);
 
                if (!result.Canceled)
                {
                    _options.Location = result.Data[LocationName].Value;
                    _options.ResourceGroup = result.Data[ResourceGroupName].Value;
                    _options.AllowResourceGroupCreation = true;
                }
                return;
            }
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Failed to enumerate available locations. Falling back to manual input.");
        }
 
        // Fallback to manual location entry using reflection on AzureLocation enum
        var locations = typeof(AzureLocation).GetProperties(BindingFlags.Public | BindingFlags.Static)
                            .Where(p => p.PropertyType == typeof(AzureLocation))
                            .Select(p => (AzureLocation)p.GetValue(null)!)
                            .Select(location => KeyValuePair.Create(location.Name, location.DisplayName ?? location.Name))
                            .OrderBy(kvp => kvp.Value)
                            .ToList();
 
        var manualResult = await _interactionService.PromptInputsAsync(
            AzureProvisioningStrings.LocationDialogTitle,
            AzureProvisioningStrings.LocationSelectionMessage,
            [
                new InteractionInput
                {
                    Name = LocationName,
                    InputType = InputType.Choice,
                    Label = AzureProvisioningStrings.LocationLabel,
                    Required = true,
                    Options = [..locations]
                },
                new InteractionInput
                {
                    Name = ResourceGroupName,
                    InputType = InputType.Text,
                    Label = AzureProvisioningStrings.ResourceGroupLabel,
                    Value = GetDefaultResourceGroupName()
                }
            ],
            new InputsDialogInteractionOptions
            {
                EnableMessageMarkdown = false,
                ValidationCallback = static (validationContext) =>
                {
                    var resourceGroupInput = validationContext.Inputs[ResourceGroupName];
                    if (!IsValidResourceGroupName(resourceGroupInput.Value))
                    {
                        validationContext.AddValidationError(resourceGroupInput, AzureProvisioningStrings.ValidationResourceGroupNameInvalid);
                    }
                    return Task.CompletedTask;
                }
            },
            cancellationToken).ConfigureAwait(false);
 
        if (!manualResult.Canceled)
        {
            _options.Location = manualResult.Data[LocationName].Value;
            _options.ResourceGroup = manualResult.Data[ResourceGroupName].Value;
            _options.AllowResourceGroupCreation = true;
        }
    }
}