File: AzureAppServiceEnvironmentExtensions.cs
Web Access
Project: src\src\Aspire.Hosting.Azure.AppService\Aspire.Hosting.Azure.AppService.csproj (Aspire.Hosting.Azure.AppService)
// 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 ASPIREPIPELINES001 // Pipeline APIs are experimental
 
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure;
using Aspire.Hosting.Azure.AppService;
using Aspire.Hosting.Pipelines;
using Azure.Core;
using Azure.Provisioning;
using Azure.Provisioning.ApplicationInsights;
using Azure.Provisioning.AppService;
using Azure.Provisioning.ContainerRegistry;
using Azure.Provisioning.Expressions;
using Azure.Provisioning.OperationalInsights;
using Azure.Provisioning.Roles;
using Microsoft.Extensions.DependencyInjection;
 
namespace Aspire.Hosting;
 
/// <summary>
/// Extensions for adding Azure App Service Environment resources to a distributed application builder.
/// </summary>
public static partial class AzureAppServiceEnvironmentExtensions
{
    internal static IDistributedApplicationBuilder AddAzureAppServiceInfrastructureCore(this IDistributedApplicationBuilder builder)
    {
        builder.AddAzureProvisioning();
 
        builder.Services.Configure<AzureProvisioningOptions>(options => options.SupportsTargetedRoleAssignments = true);
 
        // Register the pipeline step idempotently. AddAzureAppServiceInfrastructureCore can be
        // called more than once (e.g. when AddAzureAppServiceEnvironment is called for multiple
        // environments). The marker singleton ensures we only add the step the first time.
        //
        // The per-environment work (creating App Service resources and DeploymentTargetAnnotations)
        // is registered as a separate per-environment pipeline step on AzureAppServiceEnvironmentResource.
        // This global step only validates that no resource has a PublishAs* annotation when there are
        // no AzureAppServiceEnvironmentResource instances in the model.
        if (builder.Services.All(d => d.ServiceType != typeof(AppServicePipelineStepMarker)))
        {
            builder.Services.AddSingleton<AppServicePipelineStepMarker>();
 
            builder.Pipeline.AddStep(
                name: AppServicePipelineStepMarker.StepName,
                action: ctx =>
                {
                    if (!ctx.ExecutionContext.IsPublishMode)
                    {
                        return Task.CompletedTask;
                    }
 
                    if (!ctx.Model.Resources.OfType<AzureAppServiceEnvironmentResource>().Any())
                    {
                        foreach (var r in ctx.Model.GetComputeResources())
                        {
                            if (r.HasAnnotationOfType<AzureAppServiceWebsiteCustomizationAnnotation>())
                            {
                                throw new InvalidOperationException($"Resource '{r.Name}' is configured to publish as an Azure AppService Website, but there are no '{nameof(AzureAppServiceEnvironmentResource)}' resources. Ensure you have added one by calling '{nameof(AddAzureAppServiceEnvironment)}'.");
                            }
                        }
                    }
 
                    return Task.CompletedTask;
                },
                requiredBy: WellKnownPipelineSteps.BeforeStart);
        }
 
        return builder;
    }
 
    private sealed class AppServicePipelineStepMarker
    {
        public const string StepName = "validate-azure-app-service";
    }
 
    /// <summary>
    /// Adds a azure app service environment resource to the distributed application builder.
    /// </summary>
    /// <param name="builder">The distributed application builder.</param>
    /// <param name="name">The name of the resource.</param>
    /// <returns><see cref="IResourceBuilder{T}"/></returns>
    /// <ats-returns>The resource builder.</ats-returns>
    [AspireExport]
    public static IResourceBuilder<AzureAppServiceEnvironmentResource> AddAzureAppServiceEnvironment(this IDistributedApplicationBuilder builder, string name)
    {
        builder.AddAzureAppServiceInfrastructureCore();
 
        // Create the default container registry resource before creating the environment
        var registryName = $"{name}-acr";
        var defaultRegistry = CreateDefaultAzureContainerRegistry(builder, registryName);
 
        var resource = new AzureAppServiceEnvironmentResource(name, static infra =>
        {
            var resource = (AzureAppServiceEnvironmentResource)infra.AspireResource;
            var prefix = Infrastructure.NormalizeBicepIdentifier(resource.Name);
 
            // This tells azd to avoid creating infrastructure
            var userPrincipalId = new ProvisioningParameter(AzureBicepResource.KnownParameters.UserPrincipalId, typeof(string)) { Value = new BicepValue<string>(string.Empty) };
            infra.Add(userPrincipalId);
 
            var tags = new ProvisioningParameter("tags", typeof(object))
            {
                Value = new BicepDictionary<string>()
            };
 
            infra.Add(tags);
 
            UserAssignedIdentity? newIdentity = null;
            BicepValue<string> managedIdentityIdOutputValue;
            BicepValue<string> managedIdentityClientIdOutputValue;
 
            if (resource.TryGetLastAnnotation<AzureAppServiceEnvironmentAcrPullIdentityAnnotation>(out var identityAnnotation))
            {
                // The user has supplied an existing identity (commonly via AddAzureUserAssignedIdentity +
                // .WithRoleAssignments(acr, AcrPull)). Skip creating env_mi + the AcrPull role assignment
                // here and have the env module read the identity id/client id from parameters wired to the
                // identity module's outputs.
                managedIdentityIdOutputValue = identityAnnotation.Identity.Id.AsProvisioningParameter(infra);
                managedIdentityClientIdOutputValue = identityAnnotation.Identity.ClientId.AsProvisioningParameter(infra);
            }
            else
            {
                newIdentity = new UserAssignedIdentity($"{prefix}_mi")
                {
                    Tags = tags
                };
 
                infra.Add(newIdentity);
                managedIdentityIdOutputValue = newIdentity.Id.ToBicepExpression();
                managedIdentityClientIdOutputValue = newIdentity.ClientId.ToBicepExpression();
            }
 
            AzureProvisioningResource? registry = null;
            if (resource.TryGetLastAnnotation<ContainerRegistryReferenceAnnotation>(out var registryReferenceAnnotation) &&
                registryReferenceAnnotation.Registry is AzureProvisioningResource explicitRegistry)
            {
                registry = explicitRegistry;
            }
            else if (resource.DefaultContainerRegistry is not null)
            {
                registry = resource.DefaultContainerRegistry;
            }
 
            if (registry is null)
            {
                throw new InvalidOperationException($"No container registry associated with environment '{resource.Name}'. This should have been added automatically.");
            }
 
            var containerRegistry = (ContainerRegistryService)registry.AddAsExistingResource(infra);
            infra.Add(containerRegistry);
 
            if (newIdentity is not null)
            {
                var pullRa = containerRegistry.CreateRoleAssignment(ContainerRegistryBuiltInRole.AcrPull, newIdentity);
 
                // There's a bug in the CDK, see https://github.com/Azure/azure-sdk-for-net/issues/47265
                pullRa.Name = BicepFunction.CreateGuid(containerRegistry.Id, newIdentity.Id, pullRa.RoleDefinitionId);
                infra.Add(pullRa);
            }
 
            AppServicePlan plan;
            if (resource.IsExisting())
            {
                // The Aspire resource models the App Service Plan. When users mark it existing,
                // keep provisioning the supporting app resources but reference the supplied plan
                // instead of declaring a new Microsoft.Web/serverfarms resource.
                plan = (AppServicePlan)resource.AddAsExistingResource(infra);
            }
            else
            {
                plan = new AppServicePlan($"{prefix}_asplan")
                {
                    Sku = new AppServiceSkuDescription
                    {
                        Name = "P0V3",
                        Tier = "Premium"
                    },
                    Kind = "Linux",
                    IsReserved = true,
                    // Enable perSiteScaling so each app service can scale independently
                    IsPerSiteScaling = true
                };
 
                infra.Add(plan);
            }
 
            infra.Add(new ProvisioningOutput("name", typeof(string))
            {
                Value = plan.Name.ToBicepExpression()
            });
 
            infra.Add(new ProvisioningOutput("planId", typeof(string))
            {
                Value = plan.Id.ToBicepExpression()
            });
 
            infra.Add(new ProvisioningOutput("webSiteSuffix", typeof(string))
            {
                Value = AzureAppServiceEnvironmentResource.GetWebSiteSuffixBicep()
            });
 
            infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_NAME", typeof(string))
            {
                Value = containerRegistry.Name.ToBicepExpression()
            });
 
            // AZD looks for this output to find the container registry endpoint
            infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_ENDPOINT", typeof(string))
            {
                Value = containerRegistry.LoginServer.ToBicepExpression()
            });
 
            infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID", typeof(string))
            {
                Value = managedIdentityIdOutputValue
            });
 
            infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_CLIENT_ID", typeof(string))
            {
                Value = managedIdentityClientIdOutputValue
            });
 
            if (resource.EnableDashboard)
            {
                // Add aspire dashboard website
                var website = AzureAppServiceEnvironmentUtility.AddDashboard(infra, managedIdentityClientIdOutputValue, plan.Id);
                if (resource.GetDelegatedSubnetId(infra) is { } delegatedSubnetId)
                {
                    website.VirtualNetworkSubnetId = delegatedSubnetId;
                }
 
                infra.Add(new ProvisioningOutput("AZURE_APP_SERVICE_DASHBOARD_URI", typeof(string))
                {
                    Value = BicepFunction.Interpolate($"https://{AzureAppServiceEnvironmentUtility.GetDashboardHostName(resource.Name)}.azurewebsites.net")
                });
            }
 
            if (resource.EnableApplicationInsights)
            {
                ApplicationInsightsComponent? applicationInsights = null;
 
                if (resource.ApplicationInsightsResource is not null)
                {
                    applicationInsights = (ApplicationInsightsComponent)resource.ApplicationInsightsResource.AddAsExistingResource(infra);
                }
                else
                {
                    // Create Log Analytics workspace
                    var logAnalyticsWorkspace = new OperationalInsightsWorkspace($"{prefix}_law")
                    {
                        Sku = new OperationalInsightsWorkspaceSku()
                        {
                            Name = OperationalInsightsWorkspaceSkuName.PerGB2018
                        }
                    };
 
                    infra.Add(logAnalyticsWorkspace);
 
                    // Create Application Insights resource linked to the Log Analytics workspace
                    applicationInsights = new ApplicationInsightsComponent($"{prefix}_ai")
                    {
                        ApplicationType = ApplicationInsightsApplicationType.Web,
                        Kind = "web",
                        WorkspaceResourceId = logAnalyticsWorkspace.Id,
                        IngestionMode = ComponentIngestionMode.LogAnalytics
                    };
 
                    if (resource.ApplicationInsightsLocation is not null)
                    {
                        var applicationInsightsLocation = new AzureLocation(resource.ApplicationInsightsLocation);
                        applicationInsights.Location = applicationInsightsLocation;
                    }
                    else if (resource.ApplicationInsightsLocationParameter is not null)
                    {
                        var applicationInsightsLocationParameter = resource.ApplicationInsightsLocationParameter.AsProvisioningParameter(infra);
                        applicationInsights.Location = applicationInsightsLocationParameter;
                    }
                }
 
                infra.Add(applicationInsights);
 
                infra.Add(new ProvisioningOutput("AZURE_APPLICATION_INSIGHTS_INSTRUMENTATIONKEY", typeof(string))
                {
                    Value = applicationInsights.InstrumentationKey.ToBicepExpression()
                });
 
                infra.Add(new ProvisioningOutput("AZURE_APPLICATION_INSIGHTS_CONNECTION_STRING", typeof(string))
                {
                    Value = applicationInsights.ConnectionString.ToBicepExpression()
                });
            }
        })
        {
            DefaultContainerRegistry = defaultRegistry
        };
 
        // Create the resource builder first, then attach the registry to avoid recreating builders
        var appServiceEnvBuilder = builder.ExecutionContext.IsPublishMode
            ? builder.AddResource(resource).WithIconName("Globe")
            : builder.CreateResourceBuilder(resource);
 
        appServiceEnvBuilder.WithCrossScopeAcrPullIdentity(
            identity => new AzureAppServiceEnvironmentAcrPullIdentityAnnotation(identity));
 
        return appServiceEnvBuilder;
    }
 
    /// <summary>
    /// Configures whether HTTP endpoints should be automatically upgraded to HTTPS for the Azure App Service environment.
    /// By default, HTTP endpoints are upgraded to HTTPS for security and WebSocket compatibility.
    /// </summary>
    /// <param name="builder">The <see cref="IResourceBuilder{AzureAppServiceEnvironmentResource}"/> to configure.</param>
    /// <param name="upgrade">Whether to upgrade HTTP endpoints to HTTPS. Default is true.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for chaining additional configuration.</returns>
    /// <ats-returns>The resource builder.</ats-returns>
    /// <remarks>
    /// When disabled (<c>false</c>), HTTP endpoints will use HTTP scheme and port 80 in Azure App Service.
    /// Note that Azure App Service forces HTTP to HTTPS redirects at the platform level,
    /// so disabling upgrade primarily affects connection strings generated for dependent resources.
    /// </remarks>
    /// <example>
    /// Preserve HTTP endpoints instead of automatically upgrading them to HTTPS:
    /// <code>
    /// var appService = builder.AddAzureAppServiceEnvironment("appservice")
    ///     .WithHttpsUpgrade(false);
    /// </code>
    /// </example>
    [AspireExport]
    public static IResourceBuilder<AzureAppServiceEnvironmentResource> WithHttpsUpgrade(this IResourceBuilder<AzureAppServiceEnvironmentResource> builder, bool upgrade = true)
    {
        builder.Resource.PreserveHttpEndpoints = !upgrade;
        return builder;
    }
 
    /// <summary>
    /// Configures whether the Aspire dashboard should be included in the Azure App Service environment.
    /// </summary>
    /// <param name="builder">The <see cref="IResourceBuilder{AzureAppServiceEnvironmentResource}"/> to configure.</param>
    /// <param name="enable">Whether to include the Aspire dashboard. Default is true.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for chaining additional configuration.</returns>
    /// <ats-returns>The resource builder.</ats-returns>
    [AspireExport]
    public static IResourceBuilder<AzureAppServiceEnvironmentResource> WithDashboard(this IResourceBuilder<AzureAppServiceEnvironmentResource> builder, bool enable = true)
    {
        builder.Resource.EnableDashboard = enable;
        return builder;
    }
 
    /// <summary>
    /// Configures whether Azure Application Insights should be enabled for the Azure App Service.
    /// </summary>
    /// <param name="builder">The AzureAppServiceEnvironmentResource to configure.</param>
    /// <returns><see cref="IResourceBuilder{T}"/></returns>
    [AspireExportIgnore(Reason = "Polyglot AppHosts use the internal withAzureApplicationInsights dispatcher export.")]
    public static IResourceBuilder<AzureAppServiceEnvironmentResource> WithAzureApplicationInsights(this IResourceBuilder<AzureAppServiceEnvironmentResource> builder)
    {
        ArgumentNullException.ThrowIfNull(builder);
        builder.Resource.EnableApplicationInsights = true;
        return builder;
    }
 
    /// <summary>
    /// Enables Azure Application Insights for the Azure App Service environment
    /// </summary>
    [AspireExport("withAzureApplicationInsights")]
    internal static IResourceBuilder<AzureAppServiceEnvironmentResource> WithAzureApplicationInsightsForPolyglot(
        this IResourceBuilder<AzureAppServiceEnvironmentResource> builder,
        [AspireUnion(typeof(string), typeof(IResourceBuilder<ParameterResource>), typeof(IResourceBuilder<AzureApplicationInsightsResource>))] object? applicationInsights = null)
    {
        ArgumentNullException.ThrowIfNull(builder);
 
        return applicationInsights switch
        {
            null => builder.WithAzureApplicationInsights(),
            string applicationInsightsLocation => builder.WithAzureApplicationInsights(applicationInsightsLocation),
            IResourceBuilder<ParameterResource> applicationInsightsLocationParameter => builder.WithAzureApplicationInsights(applicationInsightsLocationParameter),
            IResourceBuilder<AzureApplicationInsightsResource> applicationInsightsBuilder => builder.WithAzureApplicationInsights(applicationInsightsBuilder),
            _ => throw new ArgumentException(
                "Application Insights must be omitted, a location string, a location parameter, or an Application Insights resource builder.",
                nameof(applicationInsights))
        };
    }
 
    /// <summary>
    /// Configures whether Azure Application Insights should be enabled for the Azure App Service.
    /// </summary>
    /// <param name="builder">The AzureAppServiceEnvironmentResource to configure.</param>
    /// <param name="applicationInsightsLocation">The location for Application Insights.</param>
    /// <returns><see cref="IResourceBuilder{T}"/></returns>
    [AspireExportIgnore(Reason = "Polyglot AppHosts use the internal withAzureApplicationInsights dispatcher export.")]
    public static IResourceBuilder<AzureAppServiceEnvironmentResource> WithAzureApplicationInsights(this IResourceBuilder<AzureAppServiceEnvironmentResource> builder, string applicationInsightsLocation)
    {
        builder.WithAzureApplicationInsights();
        builder.Resource.ApplicationInsightsLocation = applicationInsightsLocation;
        return builder;
    }
 
    /// <summary>
    /// Configures whether Azure Application Insights should be enabled for the Azure App Service.
    /// </summary>
    /// <param name="builder">The AzureAppServiceEnvironmentResource to configure.</param>
    /// <param name="applicationInsightsLocation">The location parameter for Application Insights.</param>
    /// <returns><see cref="IResourceBuilder{T}"/></returns>
    [AspireExportIgnore(Reason = "Polyglot AppHosts use the internal withAzureApplicationInsights dispatcher export.")]
    public static IResourceBuilder<AzureAppServiceEnvironmentResource> WithAzureApplicationInsights(this IResourceBuilder<AzureAppServiceEnvironmentResource> builder, IResourceBuilder<ParameterResource> applicationInsightsLocation)
    {
        builder.WithAzureApplicationInsights();
        builder.Resource.ApplicationInsightsLocationParameter = applicationInsightsLocation.Resource;
        return builder;
    }
 
    /// <summary>
    /// Configures whether Azure Application Insights should be enabled for the Azure App Service.
    /// </summary>
    /// <param name="builder">The AzureAppServiceEnvironmentResource builder to configure.</param>
    /// <param name="applicationInsightsBuilder">The Application Insights resource builder.</param>
    /// <returns><see cref="IResourceBuilder{T}"/></returns>
    [AspireExportIgnore(Reason = "Polyglot AppHosts use the internal withAzureApplicationInsights dispatcher export.")]
    public static IResourceBuilder<AzureAppServiceEnvironmentResource> WithAzureApplicationInsights(this IResourceBuilder<AzureAppServiceEnvironmentResource> builder, IResourceBuilder<AzureApplicationInsightsResource> applicationInsightsBuilder)
    {
        builder.WithAzureApplicationInsights();
        builder.Resource.ApplicationInsightsResource = applicationInsightsBuilder.Resource;
        return builder;
    }
 
    /// <summary>
    /// Configures the slot to which the Azure App Services should be deployed.
    /// </summary>
    /// <param name="builder">The AzureAppServiceEnvironmentResource to configure.</param>
    /// <param name="deploymentSlot">The deployment slot parameter for all App Services in the App Service Environment.</param>
    /// <returns><see cref="IResourceBuilder{T}"/></returns>
    [AspireExportIgnore(Reason = "Polyglot AppHosts use the internal withDeploymentSlot dispatcher export.")]
    public static IResourceBuilder<AzureAppServiceEnvironmentResource> WithDeploymentSlot(this IResourceBuilder<AzureAppServiceEnvironmentResource> builder, IResourceBuilder<ParameterResource> deploymentSlot)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(deploymentSlot);
 
        builder.Resource.DeploymentSlotParameter = deploymentSlot.Resource;
        return builder;
    }
 
    /// <summary>
    /// Configures the deployment slot for all Azure App Services in the environment
    /// </summary>
    [AspireExport("withDeploymentSlot")]
    internal static IResourceBuilder<AzureAppServiceEnvironmentResource> WithDeploymentSlotForPolyglot(
        this IResourceBuilder<AzureAppServiceEnvironmentResource> builder,
        [AspireUnion(typeof(string), typeof(IResourceBuilder<ParameterResource>))] object deploymentSlot)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(deploymentSlot);
 
        return deploymentSlot switch
        {
            string deploymentSlotName => builder.WithDeploymentSlot(deploymentSlotName),
            IResourceBuilder<ParameterResource> deploymentSlotParameter => builder.WithDeploymentSlot(deploymentSlotParameter),
            _ => throw new ArgumentException("Deployment slot must be a string or a parameter resource builder.", nameof(deploymentSlot))
        };
    }
 
    /// <summary>
    /// Configures the slot to which the Azure App Services should be deployed.
    /// </summary>
    /// <param name="builder">The AzureAppServiceEnvironmentResource to configure.</param>
    /// <param name="deploymentSlot">The deployment slot for all App Services in the App Service Environment.</param>
    /// <returns><see cref="IResourceBuilder{T}"/></returns>
    [AspireExportIgnore(Reason = "Polyglot AppHosts use the internal withDeploymentSlot dispatcher export.")]
    public static IResourceBuilder<AzureAppServiceEnvironmentResource> WithDeploymentSlot(this IResourceBuilder<AzureAppServiceEnvironmentResource> builder, string deploymentSlot)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrWhiteSpace(deploymentSlot);
 
        builder.Resource.DeploymentSlot = deploymentSlot;
        return builder;
    }
 
    /// <summary>
    /// Configures the Azure App Service environment to use the supplied <see cref="AzureUserAssignedIdentityResource"/>
    /// as the managed identity that App Service apps use to pull images from the configured container registry
    /// (the <c>AcrPull</c> identity), instead of having Aspire create a new identity and a new <c>AcrPull</c>
    /// role assignment.
    /// </summary>
    /// <param name="builder">The Azure App Service environment to configure.</param>
    /// <param name="identityBuilder">
    /// The resource builder for the user-assigned identity that should be used for image pulls. The supplied
    /// identity must already have the <c>AcrPull</c> role on the configured container registry.
    /// </param>
    /// <returns>The <see cref="IResourceBuilder{T}"/> for chaining.</returns>
    /// <remarks>
    /// <para>
    /// When this is set, Aspire will not create a new identity or an <c>AcrPull</c> role assignment for the
    /// container registry. The caller is responsible for ensuring the supplied identity already has the required
    /// <c>AcrPull</c> role assignment on the registry, for example by chaining
    /// <c>.WithRoleAssignments(acr, ContainerRegistryBuiltInRole.AcrPull)</c> when adding the identity.
    /// </para>
    /// <para>
    /// Unlike the equivalent method on Azure Container Apps, App Service reuses this single identity for more than
    /// just image pulls. The same identity is also attached to every generated App Service web app as a user-assigned
    /// managed identity, and is used by the Aspire OTLP sidecar to authenticate telemetry to the Aspire dashboard
    /// (it is added to the dashboard's <c>ALLOWED_MANAGED_IDENTITIES</c> and exposed to each app via the OTEL
    /// client-id app setting). If you supply an existing identity here, it will take on all of these roles.
    /// </para>
    /// <para>
    /// This is commonly combined with <c>AsExisting</c> on the App Service environment (App Service Plan) and on
    /// the container registry to deploy websites into a pre-provisioned set of Azure resources without Aspire
    /// emitting any new ACR-pull identity or ACR-pull role-assignment resources. See
    /// <see href="https://github.com/microsoft/aspire/issues/14382"/> for the scenario this addresses.
    /// </para>
    /// <para>
    /// Only the combination of an existing App Service Plan, an existing container registry, and this method
    /// avoids emitting any new identity or role-assignment resources in the env module. Other combinations still
    /// work but will emit additional resources:
    /// </para>
    /// <list type="bullet">
    ///   <item><description>
    ///   If the App Service Plan is marked <c>AsExisting</c> but no identity is supplied here, Aspire still emits
    ///   a new <c>UserAssignedIdentity</c> and an <c>AcrPull</c> role assignment on the configured registry.
    ///   </description></item>
    ///   <item><description>
    ///   If this method is used but the container registry is not existing (either the Aspire-generated default
    ///   registry or a user-added registry without <c>AsExisting</c>), the registry itself is still provisioned.
    ///   To wire the supplied identity to that newly-created registry, chain
    ///   <c>.WithRoleAssignments(acr, ContainerRegistryBuiltInRole.AcrPull)</c> on the identity.
    ///   </description></item>
    /// </list>
    /// <para>
    /// If the Aspire dashboard is enabled, Aspire still provisions the dashboard website and its contributor
    /// identity. Use <see cref="WithDashboard(IResourceBuilder{AzureAppServiceEnvironmentResource}, bool)"/> with
    /// <see langword="false"/> when targeting a fully pre-provisioned App Service Plan that should not receive a
    /// dashboard.
    /// </para>
    /// </remarks>
    /// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="identityBuilder"/> is <see langword="null"/>.</exception>
    [AspireExport]
    public static IResourceBuilder<AzureAppServiceEnvironmentResource> WithAcrPullIdentity(
        this IResourceBuilder<AzureAppServiceEnvironmentResource> builder,
        IResourceBuilder<AzureUserAssignedIdentityResource> identityBuilder)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(identityBuilder);
 
        builder.WithAnnotation(new AzureAppServiceEnvironmentAcrPullIdentityAnnotation(identityBuilder.Resource));
 
        return builder;
    }
 
    private static AzureContainerRegistryResource CreateDefaultAzureContainerRegistry(IDistributedApplicationBuilder builder, string name)
    {
        var resource = new AzureContainerRegistryResource(name, ContainerRegistryInfrastructure.ConfigureContainerRegistry);
        if (builder.ExecutionContext.IsPublishMode)
        {
            builder.AddResource(resource).WithIconName("Archive");
        }
        return resource;
    }
}