File: AzureAppServiceWebsiteContext.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.
 
using System.Globalization;
using System.Runtime.CompilerServices;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure.Utils;
using Azure.Core;
using Azure.Provisioning;
using Azure.Provisioning.AppService;
using Azure.Provisioning.Authorization;
using Azure.Provisioning.Expressions;
using Azure.Provisioning.Resources;
 
namespace Aspire.Hosting.Azure.AppService;
 
internal sealed class AzureAppServiceWebsiteContext(
    IResource resource,
    AzureAppServiceEnvironmentContext environmentContext)
{
    public IResource Resource => resource;
 
    record struct EndpointMapping(string Scheme, BicepValue<string> Host, int Port, int? TargetPort, bool IsHttpIngress, bool External);
 
    private readonly Dictionary<string, EndpointMapping> _endpointMapping = [];
    private readonly Dictionary<string, EndpointMapping> _slotEndpointMapping = [];
 
    // Resolved environment variables and command line args
    // These contain the values that need to be further transformed into
    // bicep compatible values
    public Dictionary<string, object> EnvironmentVariables { get; } = [];
    public List<object> Args { get; } = [];
 
    private AzureResourceInfrastructure? _infrastructure;
    public AzureResourceInfrastructure Infra => _infrastructure ?? throw new InvalidOperationException("Infra is not set");
 
    // Naming the app service is globally unique (domain names), so we use the resource group ID to create a unique name
    // within the naming spec for the app service.
    private BicepValue<string> HostName => BicepFunction.Take(
        BicepFunction.Interpolate($"{BicepFunction.ToLower(resource.Name)}-{AzureAppServiceEnvironmentResource.GetWebSiteSuffixBicep()}"), 60);
 
    /// <summary>
    /// Gets the hostname for a deployment slot by appending the slot name to the base website name.
    /// </summary>
    /// <param name="deploymentSlot">The deployment slot name.</param>
    /// <returns>A <see cref="BicepValue{T}"/> representing the slot hostname, truncated to 60 characters.</returns>
    public BicepValue<string> GetSlotHostName(BicepValue<string> deploymentSlot)
    {
        return BicepFunction.Take(
        BicepFunction.Interpolate($"{BicepFunction.ToLower(resource.Name)}-{AzureAppServiceEnvironmentResource.GetWebSiteSuffixBicep()}-{BicepFunction.ToLower(deploymentSlot)}"), 60);
    }
 
    public async Task ProcessAsync(CancellationToken cancellationToken)
    {
        ProcessEndpoints();
        await ProcessEnvironmentAsync(cancellationToken).ConfigureAwait(true);
        await ProcessArgumentsAsync(cancellationToken).ConfigureAwait(true);
    }
 
    private async Task ProcessEnvironmentAsync(CancellationToken cancellationToken)
    {
        if (resource.TryGetAnnotationsOfType<EnvironmentCallbackAnnotation>(out var environmentCallbacks))
        {
            var context = new EnvironmentCallbackContext(
                environmentContext.ExecutionContext, resource, EnvironmentVariables, cancellationToken);
 
            foreach (var c in environmentCallbacks)
            {
                await c.Callback(context).ConfigureAwait(true);
            }
        }
    }
 
    private async Task ProcessArgumentsAsync(CancellationToken cancellationToken)
    {
        if (resource.TryGetAnnotationsOfType<CommandLineArgsCallbackAnnotation>(out var commandLineArgsCallbackAnnotations))
        {
            var context = new CommandLineArgsCallbackContext(Args, resource, cancellationToken)
            {
                ExecutionContext = environmentContext.ExecutionContext
            };
 
            foreach (var c in commandLineArgsCallbackAnnotations)
            {
                await c.Callback(context).ConfigureAwait(true);
            }
        }
    }
 
    private void ProcessEndpoints()
    {
        // Resolve endpoint ports using the centralized helper
        var resolvedEndpoints = resource.ResolveEndpoints();
 
        if (resolvedEndpoints.Count == 0)
        {
            return;
        }
 
        // Only http/https are supported in App Service
        var unsupportedEndpoints = resolvedEndpoints.Where(r => r.Endpoint.UriScheme is not ("http" or "https")).ToArray();
        if (unsupportedEndpoints.Length > 0)
        {
            throw new NotSupportedException($"The endpoint(s) {string.Join(", ", unsupportedEndpoints.Select(r => $"'{r.Endpoint.Name}'"))} on resource '{resource.Name}' specifies an unsupported scheme. Only http and https are supported in App Service.");
        }
 
        // App Service supports only one target port
        var targetPortEndpoints = resolvedEndpoints
            .Where(r => r.Endpoint.IsExternal)
            .Select(r => r.TargetPort.Value)
            .Distinct()
            .ToList();
 
        if (targetPortEndpoints.Count > 1)
        {
            throw new NotSupportedException("App Service does not support resources with multiple external endpoints.");
        }
 
        foreach (var resolved in resolvedEndpoints)
        {
            var endpoint = resolved.Endpoint;
 
            if (!endpoint.IsExternal)
            {
                throw new NotSupportedException($"The endpoint '{endpoint.Name}' on resource '{resource.Name}' is not external. App Service only supports external endpoints.");
            }
 
            // For App Service, we ignore port mappings since ports are handled by the platform
            // TargetPort is null only for default ProjectResource endpoints (container port decides)
            _endpointMapping[endpoint.Name] = new(
                Scheme: endpoint.UriScheme,
                Host: HostName,
                Port: endpoint.UriScheme == "https" ? 443 : 80,
                TargetPort: resolved.TargetPort,
                IsHttpIngress: true,
                External: true); // All App Service endpoints are external
        }
    }
 
    private (object, SecretType) ProcessValue(object value, SecretType secretType = SecretType.None, object? parent = null, bool isSlot = false)
    {
        if (value is string s)
        {
            return (s, secretType);
        }
 
        if (value is EndpointReference ep)
        {
            var context = environmentContext.GetAppServiceContext(ep.Resource);
            return isSlot ?
                (GetEndpointValue(context._slotEndpointMapping[ep.EndpointName], EndpointProperty.Url), secretType) :
                (GetEndpointValue(context._endpointMapping[ep.EndpointName], EndpointProperty.Url), secretType);
        }
 
        if (value is ParameterResource param)
        {
            var st = param.Secret ? SecretType.Normal : secretType;
            return (AllocateParameter(param, secretType: st), st);
        }
 
        if (value is ConnectionStringReference cs)
        {
            return ProcessValue(cs.Resource.ConnectionStringExpression, secretType, parent, isSlot);
        }
 
        if (value is IResourceWithConnectionString csrs)
        {
            return ProcessValue(csrs.ConnectionStringExpression, secretType, parent, isSlot);
        }
 
        if (value is BicepOutputReference output)
        {
            return (AllocateParameter(output, secretType: secretType), secretType);
        }
 
        if (value is IAzureKeyVaultSecretReference vaultSecretReference)
        {
            if (parent is null)
            {
                return (AllocateKeyVaultSecretUriReference(vaultSecretReference), SecretType.KeyVault);
            }
 
            return (AllocateParameter(vaultSecretReference, secretType: SecretType.KeyVault), SecretType.KeyVault);
        }
 
        if (value is EndpointReferenceExpression epExpr)
        {
            var context = environmentContext.GetAppServiceContext(epExpr.Endpoint.Resource);
            var mapping = isSlot ? context._slotEndpointMapping[epExpr.Endpoint.EndpointName] : context._endpointMapping[epExpr.Endpoint.EndpointName];
            var val = GetEndpointValue(mapping, epExpr.Property);
            return (val, secretType);
        }
 
        if (value is ReferenceExpression expr)
        {
            if (expr.Format == "{0}" && expr.ValueProviders.Count == 1)
            {
                var val = ProcessValue(expr.ValueProviders[0], secretType, parent: parent, isSlot);
 
                if (expr.StringFormats[0] is string format)
                {
                    val = (BicepFormattingHelpers.FormatBicepExpression(val, format), secretType);
                }
 
                return val;
            }
 
            var args = new object[expr.ValueProviders.Count];
            var index = 0;
            var finalSecretType = SecretType.None;
 
            foreach (var vp in expr.ValueProviders)
            {
                var (val, secret) = ProcessValue(vp, secretType, expr, isSlot);
                if (secret != SecretType.None)
                {
                    finalSecretType = SecretType.Normal;
                }
 
                if (expr.StringFormats[index] is string format)
                {
                    val = BicepFormattingHelpers.FormatBicepExpression(val, format);
                }
 
                args[index++] = val;
            }
 
            return (FormattableStringFactory.Create(expr.Format, args), finalSecretType);
        }
 
        if (value is IManifestExpressionProvider manifestExpressionProvider)
        {
            return (AllocateParameter(manifestExpressionProvider, secretType), secretType);
        }
 
        throw new NotSupportedException($"Unsupported value type {value.GetType()}");
    }
 
    private static BicepValue<string> ResolveValue(object val)
    {
        return val switch
        {
            BicepValue<string> s => s,
            string s => s,
            ProvisioningParameter p => p,
            FormattableString fs => BicepFunction.Interpolate(fs),
            _ => throw new NotSupportedException($"Unsupported value type {val.GetType()}")
        };
    }
 
    public void BuildWebSite(AzureResourceInfrastructure infra)
    {
        bool buildWebAppAndSlot = resource.TryGetAnnotationsOfType<AzureAppServiceWebsiteRefreshProvisionableResourceAnnotation>(out _);
 
        _infrastructure = infra;
 
        // Check for deployment slot
        // If specified, update hostnames to endpoint references
        BicepValue<string>? deploymentSlotValue = null;
        if (environmentContext.Environment.DeploymentSlotParameter is not null || environmentContext.Environment.DeploymentSlot is not null)
        {
            deploymentSlotValue = environmentContext.Environment.DeploymentSlotParameter != null
                ? environmentContext.Environment.DeploymentSlotParameter.AsProvisioningParameter(infra)
                : environmentContext.Environment.DeploymentSlot!;
 
            UpdateHostNameForSlot(deploymentSlotValue);
        }
 
        if (deploymentSlotValue is not null && buildWebAppAndSlot)
        {
            BuildWebSiteAndSlot(infra, deploymentSlotValue!);
            return;
        }
 
        BuildWebSiteCore(infra, deploymentSlotValue);
    }
 
    /// <summary>
    /// Creates and configures an Azure App Service WebSite or WebSiteSlot.
    /// </summary>
    /// <param name="infra">The Azure resource infrastructure.</param>
    /// <param name="name">The name of the website or slot.</param>
    /// <param name="appServicePlanParameter">The App Service plan parameter.</param>
    /// <param name="acrMidParameter">The Azure Container Registry managed identity parameter.</param>
    /// <param name="acrClientIdParameter">The Azure Container Registry client ID parameter.</param>
    /// <param name="containerImage">The container image parameter.</param>
    /// <param name="isSlot">Indicates whether this is a deployment slot.</param>
    /// <param name="parentWebSite">The parent website when creating a slot.</param>
    /// <param name="deploymentSlot">The deployment slot name.</param>
    /// <returns>A dynamic object representing either a WebSite or WebSiteSlot.</returns>
    private object CreateAndConfigureWebSite(
    AzureResourceInfrastructure infra,
    BicepValue<string> name,
    BicepValue<ResourceIdentifier> appServicePlanParameter,
    BicepValue<string> acrMidParameter,
    ProvisioningParameter acrClientIdParameter,
    ProvisioningParameter containerImage,
    bool isSlot = false,
    WebSite? parentWebSite = null,
    BicepValue<string>? deploymentSlot = null)
    {
        object webSite;
        object mainContainer;
 
        if (isSlot && parentWebSite is not null && deploymentSlot is not null)
        {
            var slot = new WebSiteSlot("webappslot")
            {
                Parent = parentWebSite,
                Name = deploymentSlot,
                AppServicePlanId = appServicePlanParameter,
                SiteConfig = new SiteConfigProperties()
                {
                    LinuxFxVersion = "SITECONTAINERS",
                    AcrUserManagedIdentityId = acrClientIdParameter,
                    UseManagedIdentityCreds = true,
                    NumberOfWorkers = 30,
                    AppSettings = []
                },
                Identity = new ManagedServiceIdentity()
                {
                    ManagedServiceIdentityType = ManagedServiceIdentityType.UserAssigned,
                    UserAssignedIdentities = []
                },
            };
 
            var slotContainer = new SiteSlotSiteContainer("mainContainerSlot")
            {
                Parent = slot,
                Name = "main",
                Image = containerImage,
                AuthType = SiteContainerAuthType.UserAssigned,
                UserManagedIdentityClientId = acrClientIdParameter,
                IsMain = true
            };
 
            webSite = slot;
            mainContainer = slotContainer;
        }
        else
        {
            var site = new WebSite("webapp")
            {
                Name = name,
                AppServicePlanId = appServicePlanParameter,
                // Creating the app service with new sidecar configuration
                SiteConfig = new SiteConfigProperties()
                {
                    LinuxFxVersion = "SITECONTAINERS",
                    AcrUserManagedIdentityId = acrClientIdParameter,
                    UseManagedIdentityCreds = true,
                    // Setting NumberOfWorkers to maximum allowed value for Premium SKU
                    // https://learn.microsoft.com/en-us/azure/app-service/manage-scale-up
                    // This is required due to use of feature PerSiteScaling for the App Service plan
                    // We want the web apps to scale normally as defined for the app service plan
                    // so setting the maximum number of workers to the maximum allowed for Premium V2 SKU.
                    NumberOfWorkers = 30,
                    AppSettings = []
                },
                Identity = new ManagedServiceIdentity()
                {
                    ManagedServiceIdentityType = ManagedServiceIdentityType.UserAssigned,
                    UserAssignedIdentities = []
                },
            };
 
            // Defining the main container for the app service
            var siteContainer = new SiteContainer("mainContainer")
            {
                Parent = site,
                Name = "main",
                Image = containerImage,
                AuthType = SiteContainerAuthType.UserAssigned,
                UserManagedIdentityClientId = acrClientIdParameter,
                IsMain = true
            };
 
            webSite = site;
            mainContainer = siteContainer;
        }
 
        // There should be a single valid target port
        if (_endpointMapping.FirstOrDefault() is var (_, mapping))
        {
            var targetPort = GetEndpointValue(mapping, EndpointProperty.TargetPort);
 
            if (mainContainer is SiteContainer container)
            {
                container.TargetPort = targetPort;
            }
            else if (mainContainer is SiteSlotSiteContainer slotContainer)
            {
                slotContainer.TargetPort = targetPort;
            }
 
            if (webSite is WebSite site)
            {
                site.SiteConfig.AppSettings.Add(new AppServiceNameValuePair { Name = "WEBSITES_PORT", Value = targetPort });
            }
            else if (webSite is WebSiteSlot slot)
            {
                slot.SiteConfig.AppSettings.Add(new AppServiceNameValuePair { Name = "WEBSITES_PORT", Value = targetPort });
            }
        }
 
        foreach (var kv in EnvironmentVariables)
        {
            var (val, secretType) = ProcessValue(kv.Value, isSlot: isSlot);
            var value = ResolveValue(val);
 
            if (secretType == SecretType.KeyVault)
            {
                // https://learn.microsoft.com/azure/app-service/app-service-key-vault-references?tabs=azure-cli#-understand-source-app-settings-from-key-vault
                // @Microsoft.KeyVault({referenceString})
                value = BicepFunction.Interpolate($"@Microsoft.KeyVault(SecretUri={val})");
            }
 
            if (webSite is WebSite site)
            {
                site.SiteConfig.AppSettings.Add(new AppServiceNameValuePair { Name = kv.Key, Value = value });
            }
            else if (webSite is WebSiteSlot slot)
            {
                slot.SiteConfig.AppSettings.Add(new AppServiceNameValuePair { Name = kv.Key, Value = value });
            }
        }
 
        if (Args.Count > 0)
        {
            var args = new List<BicepValue<string>>();
 
            foreach (var arg in Args)
            {
                var (val, secretType) = ProcessValue(arg);
                var value = ResolveValue(val);
 
                args.Add(value);
            }
 
            // App Service does not support array arguments, so we need to join them into a single string
            static FunctionCallExpression Join(BicepExpression args, string delimeter) =>
                new(new IdentifierExpression("join"), args, new StringLiteralExpression(delimeter));
 
            var arrayExpression = new ArrayExpression([.. args.Select(a => a.Compile())]);
 
            if (mainContainer is SiteContainer container)
            {
                container.StartUpCommand = Join(arrayExpression, " ");
            }
            else if (mainContainer is SiteSlotSiteContainer slotContainer)
            {
                slotContainer.StartUpCommand = Join(arrayExpression, " ");
            }
        }
 
        if (mainContainer is SiteContainer mainSiteContainer)
        {
            infra.Add(mainSiteContainer);
        }
        else if (mainContainer is SiteSlotSiteContainer mainSiteSlotContainer)
        {
            infra.Add(mainSiteSlotContainer);
        }
 
        var id = BicepFunction.Interpolate($"{acrMidParameter}").Compile().ToString();
 
        if (webSite is WebSite siteObject)
        {
            siteObject.Identity.UserAssignedIdentities[id] = new UserAssignedIdentityDetails();
        }
        else if (webSite is WebSiteSlot slot)
        {
            slot.Identity.UserAssignedIdentities[id] = new UserAssignedIdentityDetails();
        }
 
        // This is the user assigned identity associated with the web app, not the container registry
        if (resource.TryGetLastAnnotation<AppIdentityAnnotation>(out var appIdentityAnnotation))
        {
            var appIdentityResource = appIdentityAnnotation.IdentityResource;
 
            var computeIdentity = appIdentityResource.Id.AsProvisioningParameter(infra);
 
            var cid = BicepFunction.Interpolate($"{computeIdentity}").Compile().ToString();
 
            if (webSite is WebSite site)
            {
                site.KeyVaultReferenceIdentity = computeIdentity;
                site.Identity.ManagedServiceIdentityType = ManagedServiceIdentityType.UserAssigned;
                site.Identity.UserAssignedIdentities[cid] = new UserAssignedIdentityDetails();
 
                site.SiteConfig.AppSettings.Add(new AppServiceNameValuePair
                {
                    Name = "AZURE_CLIENT_ID",
                    Value = appIdentityResource.ClientId.AsProvisioningParameter(infra)
                });
 
                // DefaultAzureCredential should only use ManagedIdentityCredential when running in Azure
                site.SiteConfig.AppSettings.Add(new AppServiceNameValuePair
                {
                    Name = "AZURE_TOKEN_CREDENTIALS",
                    Value = "ManagedIdentityCredential"
                });
            }
            else if (webSite is WebSiteSlot slot)
            {
                slot.KeyVaultReferenceIdentity = computeIdentity;
                slot.Identity.UserAssignedIdentities[id] = new UserAssignedIdentityDetails();
                slot.Identity.ManagedServiceIdentityType = ManagedServiceIdentityType.UserAssigned;
                slot.Identity.UserAssignedIdentities[cid] = new UserAssignedIdentityDetails();
                slot.SiteConfig.AppSettings.Add(new AppServiceNameValuePair
                {
                    Name = "AZURE_CLIENT_ID",
                    Value = appIdentityResource.ClientId.AsProvisioningParameter(infra)
                });
                // DefaultAzureCredential should only use ManagedIdentityCredential when running in Azure
                slot.SiteConfig.AppSettings.Add(new AppServiceNameValuePair
                {
                    Name = "AZURE_TOKEN_CREDENTIALS",
                    Value = "ManagedIdentityCredential"
                });
            }
        }
 
        // Added appsetting to identify the resource in a specific aspire environment
        if (webSite is WebSite siteObj)
        {
            siteObj.SiteConfig.AppSettings.Add(new AppServiceNameValuePair { Name = "ASPIRE_ENVIRONMENT_NAME", Value = environmentContext.Environment.Name });
        }
        else if (webSite is WebSiteSlot slot)
        {
            slot.SiteConfig.AppSettings.Add(new AppServiceNameValuePair { Name = "ASPIRE_ENVIRONMENT_NAME", Value = environmentContext.Environment.Name });
        }
 
        // Probes
#pragma warning disable ASPIREPROBES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
        if (resource.TryGetAnnotationsOfType<ProbeAnnotation>(out var probeAnnotations))
        {
            // AppService allow only one "health check" with only path, so prioritize "liveness" and/or take the first one
            var endpointProbeAnnotation = probeAnnotations
                .OfType<EndpointProbeAnnotation>()
                .OrderBy(probeAnnotation => probeAnnotation.Type == ProbeType.Liveness ? 0 : 1)
                .FirstOrDefault();
 
            if (endpointProbeAnnotation is not null)
            {
                if (webSite is WebSiteSlot slot)
                {
                    slot.SiteConfig.HealthCheckPath = endpointProbeAnnotation.Path;
                }
                else if (webSite is WebSite site)
                {
                    site.SiteConfig.HealthCheckPath = endpointProbeAnnotation.Path;
                }
            }
        }
#pragma warning restore ASPIREPROBES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
 
        RoleAssignment? webSiteRa = null;
        if (environmentContext.Environment.EnableDashboard)
        {
            webSiteRa = AddDashboardPermissionAndSettings(webSite, acrClientIdParameter, deploymentSlot);
        }
 
        if (webSite is WebSite webSiteObject)
        {
            infra.Add(webSiteObject);
        }
        else if (webSite is WebSiteSlot slot)
        {
            infra.Add(slot);
        }
 
        if (webSiteRa is not null)
        {
            infra.Add(webSiteRa);
        }
 
        if (environmentContext.Environment.EnableApplicationInsights)
        {
            EnableApplicationInsightsForWebSite(webSite);
        }
 
        return webSite;
    }
 
    /// <summary>
    /// Builds either a standalone website or a deployment slot based on whether a deployment slot is specified.
    /// </summary>
    /// <param name="infra">The Azure resource infrastructure.</param>
    /// <param name="deploymentSlot">The optional deployment slot name.</param>
    private void BuildWebSiteCore(
        AzureResourceInfrastructure infra,
        BicepValue<string>? deploymentSlot = null)
    {
        _infrastructure = infra;
 
        _ = environmentContext.Environment.ContainerRegistryUrl.AsProvisioningParameter(infra);
        var appServicePlanParameter = environmentContext.Environment.PlanIdOutputReference.AsProvisioningParameter(infra);
        var acrMidParameter = environmentContext.Environment.ContainerRegistryManagedIdentityId.AsProvisioningParameter(infra);
        var acrClientIdParameter = environmentContext.Environment.ContainerRegistryClientId.AsProvisioningParameter(infra);
        var containerImage = AllocateParameter(new ContainerImageReference(Resource));
 
        // Create parent WebSite from existing
        WebSite? parentWebSite = null;
 
        if (deploymentSlot is not null)
        {
            parentWebSite = WebSite.FromExisting("webapp");
            parentWebSite.Name = HostName;
            Infra.Add(parentWebSite);
        }
 
        var webSite = CreateAndConfigureWebSite(
            infra,
            HostName,
            appServicePlanParameter,
            acrMidParameter,
            acrClientIdParameter,
            containerImage,
            isSlot: deploymentSlot is not null,
            parentWebSite: parentWebSite,
            deploymentSlot: deploymentSlot);
 
        // Allow users to customize the web app here
        if (deploymentSlot is not null)
        {
            if (resource.TryGetAnnotationsOfType<AzureAppServiceWebsiteSlotCustomizationAnnotation>(out var customizeWebSiteSlotAnnotations))
            {
                var slot = (WebSiteSlot)webSite;
                foreach (var customizeWebSiteSlotAnnotation in customizeWebSiteSlotAnnotations)
                {
                    customizeWebSiteSlotAnnotation.Configure(Infra, slot);
                }
            }
        }
        else
        {
            if (resource.TryGetAnnotationsOfType<AzureAppServiceWebsiteCustomizationAnnotation>(out var customizeWebSiteAnnotations))
            {
                var site = (WebSite)webSite;
                foreach (var customizeWebSiteAnnotation in customizeWebSiteAnnotations)
                {
                    customizeWebSiteAnnotation.Configure(infra, site);
                }
            }
        }
    }
 
    /// <summary>
    /// Builds both the main website and a deployment slot when deploying to a slot for the first time.
    /// </summary>
    /// <param name="infra">The Azure resource infrastructure.</param>
    /// <param name="deploymentSlot">The deployment slot name.</param>
    private void BuildWebSiteAndSlot(
        AzureResourceInfrastructure infra,
        BicepValue<string> deploymentSlot)
    {
        _infrastructure = infra;
 
        _ = environmentContext.Environment.ContainerRegistryUrl.AsProvisioningParameter(infra);
        var appServicePlanParameter = environmentContext.Environment.PlanIdOutputReference.AsProvisioningParameter(infra);
        var acrMidParameter = environmentContext.Environment.ContainerRegistryManagedIdentityId.AsProvisioningParameter(infra);
        var acrClientIdParameter = environmentContext.Environment.ContainerRegistryClientId.AsProvisioningParameter(infra);
        var containerImage = AllocateParameter(new ContainerImageReference(Resource));
 
        // Main site
        var webSite = (WebSite)CreateAndConfigureWebSite(
            infra,
            HostName,
            appServicePlanParameter,
            acrMidParameter,
            acrClientIdParameter,
            containerImage,
            isSlot: false);
 
        // Slot
        var webSiteSlot = (WebSiteSlot)CreateAndConfigureWebSite(
            infra,
            deploymentSlot,
            appServicePlanParameter,
            acrMidParameter,
            acrClientIdParameter,
            containerImage,
            isSlot: true,
            parentWebSite: (WebSite)webSite,
            deploymentSlot: deploymentSlot);
 
        // Allow users to customize the website
        if (resource.TryGetAnnotationsOfType<AzureAppServiceWebsiteCustomizationAnnotation>(out var customizeWebSiteAnnotations))
        {
            foreach (var customizeWebSiteAnnotation in customizeWebSiteAnnotations)
            {
                customizeWebSiteAnnotation.Configure(infra, webSite);
            }
        }
 
        // Allow users to customize the slot
        if (resource.TryGetAnnotationsOfType<AzureAppServiceWebsiteSlotCustomizationAnnotation>(out var customizeWebSiteSlotAnnotations))
        {
            foreach (var customizeWebSiteSlotAnnotation in customizeWebSiteSlotAnnotations)
            {
                customizeWebSiteSlotAnnotation.Configure(infra, webSiteSlot);
            }
        }
    }
 
    private BicepValue<string> GetEndpointValue(EndpointMapping mapping, EndpointProperty property)
    {
        return property switch
        {
            EndpointProperty.Url => BicepFunction.Interpolate($"{mapping.Scheme}://{mapping.Host}.azurewebsites.net"),
            EndpointProperty.Host => BicepFunction.Interpolate($"{mapping.Host}.azurewebsites.net"),
            EndpointProperty.Port => mapping.Port.ToString(CultureInfo.InvariantCulture),
            EndpointProperty.TargetPort => mapping.TargetPort?.ToString(CultureInfo.InvariantCulture) ?? (BicepValue<string>)AllocateParameter(new ContainerPortReference(Resource)),
            EndpointProperty.Scheme => mapping.Scheme,
            EndpointProperty.HostAndPort => BicepFunction.Interpolate($"{mapping.Host}.azurewebsites.net"),
            EndpointProperty.IPV4Host => BicepFunction.Interpolate($"{mapping.Host}.azurewebsites.net"),
            _ => throw new NotSupportedException($"Unsupported endpoint property {property}")
        };
    }
 
    private BicepValue<string> AllocateKeyVaultSecretUriReference(IAzureKeyVaultSecretReference secretReference)
    {
        var secret = secretReference.AsKeyVaultSecret(Infra);
 
        // https://learn.microsoft.com/azure/app-service/app-service-key-vault-references?tabs=azure-cli#-understand-source-app-settings-from-key-vault
        return secret.Properties.SecretUri;
    }
 
    private ProvisioningParameter AllocateParameter(IManifestExpressionProvider parameter, SecretType secretType = SecretType.None)
    {
        return parameter.AsProvisioningParameter(Infra, isSecure: secretType == SecretType.Normal);
    }
 
    private RoleAssignment AddDashboardPermissionAndSettings(object webSite, ProvisioningParameter acrClientIdParameter, BicepValue<string>? deploymentSlot = null)
    {
        bool isSlot = deploymentSlot is not null;
        var dashboardUri = environmentContext.Environment.DashboardUriReference.AsProvisioningParameter(Infra);
        var contributorId = environmentContext.Environment.WebsiteContributorManagedIdentityId.AsProvisioningParameter(Infra);
        var contributorPrincipalId = environmentContext.Environment.WebsiteContributorManagedIdentityPrincipalId.AsProvisioningParameter(Infra);
        var otelServiceName = isSlot ? BicepFunction.Interpolate($"{resource.Name}-{deploymentSlot}") : resource.Name;
 
        // Add Website Contributor role assignment to dashboard's managed identity for this webapp
        var websiteRaId = BicepFunction.GetSubscriptionResourceId(
                    "Microsoft.Authorization/roleDefinitions",
                    "de139f84-1756-47ae-9be6-808fbbe84772");
 
        string raResourceName = isSlot
            ? Infrastructure.NormalizeBicepIdentifier($"{Infra.AspireResource.Name}_slot_ra")
            : Infrastructure.NormalizeBicepIdentifier($"{Infra.AspireResource.Name}_ra");
 
        RoleAssignment? roleAssignment = null;
 
        if (webSite is WebSite site)
        {
            // Add the appsettings specific to sending telemetry data to dashboard
            site.SiteConfig.AppSettings.Add(new AppServiceNameValuePair { Name = "OTEL_SERVICE_NAME", Value = otelServiceName });
            site.SiteConfig.AppSettings.Add(new AppServiceNameValuePair { Name = "OTEL_EXPORTER_OTLP_PROTOCOL", Value = "grpc" });
            site.SiteConfig.AppSettings.Add(new AppServiceNameValuePair { Name = "OTEL_EXPORTER_OTLP_ENDPOINT", Value = "http://localhost:6001" });
            site.SiteConfig.AppSettings.Add(new AppServiceNameValuePair { Name = "WEBSITE_ENABLE_ASPIRE_OTEL_SIDECAR", Value = "true" });
            site.SiteConfig.AppSettings.Add(new AppServiceNameValuePair { Name = "OTEL_COLLECTOR_URL", Value = dashboardUri });
            site.SiteConfig.AppSettings.Add(new AppServiceNameValuePair { Name = "OTEL_CLIENT_ID", Value = acrClientIdParameter });
 
            var websiteRaName = BicepFunction.CreateGuid(site.Id, contributorId, websiteRaId);
            roleAssignment = new RoleAssignment(raResourceName)
            {
                Name = websiteRaName,
                Scope = new IdentifierExpression(site.BicepIdentifier),
                PrincipalType = RoleManagementPrincipalType.ServicePrincipal,
                PrincipalId = contributorPrincipalId,
                RoleDefinitionId = websiteRaId,
            };
        }
        else if (webSite is WebSiteSlot slot)
        {
            // Add the appsettings specific to sending telemetry data to dashboard
            slot.SiteConfig.AppSettings.Add(new AppServiceNameValuePair { Name = "OTEL_SERVICE_NAME", Value = otelServiceName });
            slot.SiteConfig.AppSettings.Add(new AppServiceNameValuePair { Name = "OTEL_EXPORTER_OTLP_PROTOCOL", Value = "grpc" });
            slot.SiteConfig.AppSettings.Add(new AppServiceNameValuePair { Name = "OTEL_EXPORTER_OTLP_ENDPOINT", Value = "http://localhost:6001" });
            slot.SiteConfig.AppSettings.Add(new AppServiceNameValuePair { Name = "WEBSITE_ENABLE_ASPIRE_OTEL_SIDECAR", Value = "true" });
            slot.SiteConfig.AppSettings.Add(new AppServiceNameValuePair { Name = "OTEL_COLLECTOR_URL", Value = dashboardUri });
            slot.SiteConfig.AppSettings.Add(new AppServiceNameValuePair { Name = "OTEL_CLIENT_ID", Value = acrClientIdParameter });
 
            var websiteRaName = BicepFunction.CreateGuid(slot.Id, contributorId, websiteRaId);
            roleAssignment = new RoleAssignment(raResourceName)
            {
                Name = websiteRaName,
                Scope = new IdentifierExpression(slot.BicepIdentifier),
                PrincipalType = RoleManagementPrincipalType.ServicePrincipal,
                PrincipalId = contributorPrincipalId,
                RoleDefinitionId = websiteRaId,
            };
        }
 
        return roleAssignment!;
    }
 
    private void EnableApplicationInsightsForWebSite(object webSite)
    {
        var appInsightsInstrumentationKey = environmentContext.Environment.AzureAppInsightsInstrumentationKeyReference.AsProvisioningParameter(Infra);
        var appInsightsConnectionString = environmentContext.Environment.AzureAppInsightsConnectionStringReference.AsProvisioningParameter(Infra);
 
        if (webSite is WebSite site)
        {
            site.SiteConfig.AppSettings.Add(new AppServiceNameValuePair
            {
                Name = "APPINSIGHTS_INSTRUMENTATIONKEY",
                Value = appInsightsInstrumentationKey
            });
            site.SiteConfig.AppSettings.Add(new AppServiceNameValuePair
            {
                Name = "APPLICATIONINSIGHTS_CONNECTION_STRING",
                Value = appInsightsConnectionString
            });
            site.SiteConfig.AppSettings.Add(new AppServiceNameValuePair
            {
                Name = "ApplicationInsightsAgent_EXTENSION_VERSION",
                Value = "~3"
            });
        }
        else if (webSite is WebSiteSlot slot)
        {
            // Website configuration for Application Insights
            slot.SiteConfig.AppSettings.Add(new AppServiceNameValuePair
            {
                Name = "APPINSIGHTS_INSTRUMENTATIONKEY",
                Value = appInsightsInstrumentationKey
            });
 
            slot.SiteConfig.AppSettings.Add(new AppServiceNameValuePair
            {
                Name = "APPLICATIONINSIGHTS_CONNECTION_STRING",
                Value = appInsightsConnectionString
            });
 
            slot.SiteConfig.AppSettings.Add(new AppServiceNameValuePair
            {
                Name = "ApplicationInsightsAgent_EXTENSION_VERSION",
                Value = "~3"
            });
        }                                                  
    }
 
    /// <summary>
    /// Updates endpoint mappings to use deployment slot hostnames.
    /// </summary>
    /// <param name="slotName">The deployment slot name.</param>
    private void UpdateHostNameForSlot(BicepValue<string> slotName)
    {
        foreach (var (name, mapping) in _endpointMapping.ToList())
        {
            BicepValue<string> hostValue;
 
            hostValue = GetSlotHostName(slotName);
            _slotEndpointMapping[name] = mapping with { Host = hostValue };
        }
    }
 
    enum SecretType
    {
        None,
        Normal,
        KeyVault
    }
}