File: AzureApiManagementExtensions.cs
Web Access
Project: src\src\Aspire.Hosting.Azure.ApiManagement\Aspire.Hosting.Azure.ApiManagement.csproj (Aspire.Hosting.Azure.ApiManagement)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.IO.Hashing;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure;
using Aspire.Hosting.Foundry;
using Aspire.Hosting.Pipelines;
using Azure.Provisioning;
using Azure.Provisioning.ApiManagement;
using Azure.Provisioning.ApplicationInsights;
using Azure.Provisioning.Authorization;
using Azure.Provisioning.CognitiveServices;
using Azure.Provisioning.Expressions;
using Azure.Provisioning.KeyVault;
using Azure.Provisioning.Network;
using Azure.Provisioning.Primitives;
using Azure.Provisioning.Resources;
using Azure.Provisioning.Roles;
using Azure.Provisioning.Storage;
using Microsoft.Extensions.DependencyInjection;
using SubscriptionState = Azure.Provisioning.ApiManagement.SubscriptionState;
 
namespace Aspire.Hosting;
 
/// <summary>
/// Provides extension methods for adding Azure API Management resources to the application model.
/// </summary>
public static class AzureApiManagementExtensions
{
    private const string ProxyOperationName = "proxy";
    private const string PremiumV2SubnetDelegation = "Microsoft.Web/hostingEnvironments";
    private static readonly char[] s_invalidApiIdentifierCharacters = ['*', '#', '&', '+', ':', '<', '>', '?'];
    private static readonly string[] s_proxyOperationMethods = ["DELETE", "GET", "HEAD", "OPTIONS", "PATCH", "POST", "PUT", "TRACE"];
 
    /// <summary>
    /// Adds an Azure API Management service.
    /// </summary>
    /// <param name="builder">The distributed application builder.</param>
    /// <param name="name">The name of the Aspire resource.</param>
    /// <param name="options">The API Management service options.</param>
    /// <returns>A builder for the Azure API Management resource.</returns>
    /// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="options"/> is null.</exception>
    /// <exception cref="ArgumentException">Thrown when required option values are empty or capacity is invalid for the selected SKU.</exception>
    /// <example>
    /// <code>
    /// var apim = builder.AddAzureApiManagement("apim", new()
    /// {
    ///     PublisherEmail = "api-owners@example.com",
    ///     PublisherName = "Contoso APIs",
    ///     Sku = AzureApiManagementSku.StandardV2,
    /// });
    /// </code>
    /// </example>
    [AspireExport]
    public static IResourceBuilder<AzureApiManagementResource> AddAzureApiManagement(
        this IDistributedApplicationBuilder builder,
        [ResourceName] string name,
        AzureApiManagementOptions options)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(name);
        ArgumentNullException.ThrowIfNull(options);
        ArgumentException.ThrowIfNullOrEmpty(options.PublisherEmail);
        ArgumentException.ThrowIfNullOrEmpty(options.PublisherName);
        ValidateMaximumLength(options.PublisherEmail, 100, "The publisher email address", nameof(options));
        ValidateMaximumLength(options.PublisherName, 100, "The publisher name", nameof(options));
        ValidateCapacity(options.Sku, options.Capacity);
 
        var resource = new AzureApiManagementResource(name, options, ConfigureInfrastructure);
        resource.Annotations.Add(new PipelineConfigurationAnnotation(context =>
        {
            var provisionSteps = context.GetSteps(resource, WellKnownPipelineTags.ProvisionInfrastructure);
            var openApiDeploymentTargets = resource.Apis
                .Where(api => api.OpenApiSource is AzureApiManagementOpenApiEndpoint)
                .Select(api => api.Target)
                .OfType<IResource>()
                .Distinct();
            foreach (var target in openApiDeploymentTargets)
            {
                if (!ComputeEnvironmentEndpointResolver.TryGetEffectiveComputeEnvironment(target, out var computeEnvironment) ||
                    target.GetDeploymentTargetAnnotation(computeEnvironment)?.DeploymentTarget is not { } deploymentTarget)
                {
                    // Deployment targets are materialized by the compute environment's BeforeStart step.
                    // The callback runs once before that step and again when the deploy pipeline is resolved.
                    continue;
                }
 
                if (deploymentTarget is not AzureProvisioningResource azureDeploymentTarget)
                {
                    throw new InvalidOperationException(
                        $"Resource '{target.Name}' uses a compute publisher that cannot be ordered before API Management OpenAPI import. " +
                        "Use WithOpenApiDocument instead.");
                }
 
                // Azure provisioning resources use this stable step name. Referencing it directly also works
                // when the compute environment expands deployment-target steps after this callback executes.
                provisionSteps.DependsOn($"provision-{azureDeploymentTarget.Name}");
            }
        }));
 
        if (builder.ExecutionContext.IsRunMode)
        {
            return builder.CreateResourceBuilder(resource);
        }
 
        builder.AddAzureProvisioning();
        builder.Services.Configure<AzureProvisioningOptions>(provisioning => provisioning.SupportsTargetedRoleAssignments = true);
        builder.AddResource(resource.Identity).WithParentRelationship(resource);
        resource.References.Add(resource.Identity);
 
        return builder.AddResource(resource)
            .WithIconName("GlobeShield");
    }
 
    /// <summary>
    /// Configures the user-assigned managed identity used by API Management.
    /// </summary>
    /// <param name="builder">The Azure API Management resource builder.</param>
    /// <param name="identityResourceBuilder">The user-assigned managed identity.</param>
    /// <returns>The Azure API Management resource builder.</returns>
    /// <remarks>
    /// New services attach the supplied identity automatically instead of creating a default identity.
    /// For an existing service, the supplied identity must be configured as an existing resource and
    /// already attached to the service outside Aspire. Aspire does not change or verify the existing
    /// service's identity attachments. Backend and Key Vault permissions use the supplied identity.
    /// </remarks>
    /// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="identityResourceBuilder"/> is null.</exception>
    [AspireExport]
    public static IResourceBuilder<AzureApiManagementResource> WithAzureUserAssignedIdentity(
        this IResourceBuilder<AzureApiManagementResource> builder,
        IResourceBuilder<AzureUserAssignedIdentityResource> identityResourceBuilder)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(identityResourceBuilder);
 
        var previousIdentity = builder.Resource.Identity;
        if (ReferenceEquals(previousIdentity, identityResourceBuilder.Resource))
        {
            return builder;
        }
 
        // Only the automatically created identity belongs to APIM; never remove a caller-owned identity.
        if (builder.Resource.HasDefaultIdentity)
        {
            builder.ApplicationBuilder.Resources.Remove(previousIdentity);
        }
        builder.Resource.SetIdentity(identityResourceBuilder.Resource);
        builder.WithAnnotation(new AppIdentityAnnotation(identityResourceBuilder.Resource), ResourceAnnotationMutationBehavior.Replace);
 
        return builder;
    }
 
    /// <summary>
    /// Adds an API that routes requests to an Aspire compute resource.
    /// </summary>
    /// <typeparam name="T">The backend compute resource type.</typeparam>
    /// <param name="builder">The Azure API Management resource builder.</param>
    /// <param name="name">The globally unique Aspire resource name and default physical API name.</param>
    /// <param name="target">
    /// The backend compute resource. It must expose an HTTP or HTTPS endpoint. Internal endpoints require the
    /// API Management service to have virtual network connectivity to the target.
    /// </param>
    /// <param name="path">The public path beneath the API Management gateway hostname.</param>
    /// <param name="displayName">The API display name. The resource name is used when omitted.</param>
    /// <param name="subscriptionRequired">Whether callers must provide an API Management subscription key. The default is <see langword="true"/>.</param>
    /// <param name="apiName">The physical API identifier in API Management. The Aspire resource name is used when omitted.</param>
    /// <returns>A builder for the API resource.</returns>
    /// <remarks>
    /// The generated API contains catch-all operations for supported HTTP methods and paths. The generated policy routes
    /// requests through an API Management backend entity whose URL is resolved from the target deployment environment.
    /// </remarks>
    /// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="target"/> is null.</exception>
    /// <exception cref="ArgumentException">Thrown when a required string is empty.</exception>
    [AspireExport]
    public static IResourceBuilder<AzureApiManagementApiResource> AddApi<T>(
        this IResourceBuilder<AzureApiManagementResource> builder,
        [ResourceName] string name,
        IResourceBuilder<T> target,
        string path,
        string? displayName = null,
        bool subscriptionRequired = true,
        string? apiName = null)
        where T : IComputeResource, IResourceWithEndpoints
    {
        ArgumentNullException.ThrowIfNull(target);
        return AddApiCore(builder, name, path, displayName, subscriptionRequired, apiName, target.Resource);
    }
 
    /// <summary>
    /// Adds an API whose backend is configured separately.
    /// </summary>
    /// <param name="builder">The Azure API Management resource builder.</param>
    /// <param name="name">The globally unique Aspire resource name and default physical API name.</param>
    /// <param name="path">The public path beneath the API Management gateway hostname.</param>
    /// <param name="displayName">The API display name. The resource name is used when omitted.</param>
    /// <param name="subscriptionRequired">Whether callers must provide an API Management subscription key. The default is <see langword="true"/>.</param>
    /// <param name="apiName">The physical API identifier in API Management. The Aspire resource name is used when omitted.</param>
    /// <returns>A builder for the API resource.</returns>
    /// <remarks>
    /// Configure the API backend with <see cref="WithBackend(IResourceBuilder{AzureApiManagementApiResource}, IResourceBuilder{AzureApiManagementBackendResource})"/>
    /// or <see cref="WithBackend(IResourceBuilder{AzureApiManagementApiResource}, IResourceBuilder{AzureApiManagementBackendPoolResource})"/>.
    /// </remarks>
    [AspireExport("addApiWithoutTarget")]
    public static IResourceBuilder<AzureApiManagementApiResource> AddApi(
        this IResourceBuilder<AzureApiManagementResource> builder,
        [ResourceName] string name,
        string path,
        string? displayName = null,
        bool subscriptionRequired = true,
        string? apiName = null)
    {
        return AddApiCore(builder, name, path, displayName, subscriptionRequired, apiName);
    }
 
    /// <summary>
    /// Imports an OpenAPI document from a file into an API.
    /// </summary>
    /// <param name="builder">The API Management API resource builder.</param>
    /// <param name="documentPath">The document path, relative to the AppHost directory when not absolute.</param>
    /// <param name="format">The document format. The file extension is used when omitted.</param>
    /// <returns>The API Management API resource builder.</returns>
    /// <remarks>
    /// The document is read when generating infrastructure for publish or deploy, never during run mode.
    /// It must exist and contain non-whitespace content by the time infrastructure is generated.
    /// An API can have only one OpenAPI source: either a document file or an endpoint.
    /// Repeating the same source configuration is allowed; configuring a different source throws.
    /// </remarks>
    /// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> is null.</exception>
    /// <exception cref="ArgumentException">Thrown when <paramref name="documentPath"/> is empty or its extension cannot determine the format.</exception>
    /// <exception cref="InvalidOperationException">Thrown when a different OpenAPI source is already configured.</exception>
    [AspireExport]
    public static IResourceBuilder<AzureApiManagementApiResource> WithOpenApiDocument(
        this IResourceBuilder<AzureApiManagementApiResource> builder,
        string documentPath,
        AzureApiManagementOpenApiFormat? format = null)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(documentPath);
 
        var fullPath = Path.GetFullPath(documentPath, builder.ApplicationBuilder.AppHostDirectory);
        builder.Resource.SetOpenApiSource(new AzureApiManagementOpenApiFile(
            fullPath,
            format ?? InferOpenApiFormat(documentPath)));
        return builder;
    }
 
    /// <summary>
    /// Imports an OpenAPI document exposed by the API's target compute resource.
    /// </summary>
    /// <param name="builder">The API Management API resource builder.</param>
    /// <param name="documentPath">The absolute path of the OpenAPI endpoint, such as <c>/openapi/v1.json</c>.</param>
    /// <param name="endpointName">The target endpoint name. An external HTTP or HTTPS endpoint is selected when omitted.</param>
    /// <param name="format">The document format. The endpoint path extension is used when omitted.</param>
    /// <returns>The API Management API resource builder.</returns>
    /// <remarks>
    /// Azure API Management retrieves linked documents through its control plane, so the selected endpoint must be
    /// externally reachable during deployment. Use <see cref="WithOpenApiDocument"/> for private backends.
    /// An API can have only one OpenAPI source: either a document file or an endpoint.
    /// Repeating the same source configuration is allowed; configuring a different source throws.
    /// </remarks>
    /// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> is null.</exception>
    /// <exception cref="ArgumentException">Thrown when <paramref name="documentPath"/> is empty or is not an absolute path.</exception>
    /// <exception cref="InvalidOperationException">Thrown when a different OpenAPI source is already configured, or the API has no target compute resource or no suitable external endpoint.</exception>
    [AspireExport]
    public static IResourceBuilder<AzureApiManagementApiResource> WithOpenApiEndpoint(
        this IResourceBuilder<AzureApiManagementApiResource> builder,
        string documentPath = "/openapi/v1.json",
        string? endpointName = null,
        AzureApiManagementOpenApiFormat? format = null)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(documentPath);
 
        if (!documentPath.StartsWith("/", StringComparison.Ordinal))
        {
            throw new ArgumentException("The OpenAPI endpoint path must start with '/'.", nameof(documentPath));
        }
 
        if (builder.Resource.Target is null)
        {
            throw new InvalidOperationException(
                $"API Management API '{builder.Resource.Name}' does not have a target compute resource.");
        }
 
        builder.Resource.SetOpenApiSource(new AzureApiManagementOpenApiEndpoint(
            documentPath,
            endpointName,
            format ?? InferOpenApiFormat(documentPath)));
        return builder;
    }
 
    /// <summary>
    /// Adds an OpenAI-compatible API backed by an API Management load-balancing pool.
    /// </summary>
    /// <param name="builder">The Azure API Management resource builder.</param>
    /// <param name="name">The globally unique Aspire resource name and default physical API name.</param>
    /// <param name="path">The public path beneath the API Management gateway hostname.</param>
    /// <param name="displayName">The API display name. The resource name is used when omitted.</param>
    /// <param name="subscriptionRequired">Whether callers must provide an API Management subscription key. The default is <see langword="true"/>.</param>
    /// <param name="apiName">The physical API identifier in API Management. The Aspire resource name is used when omitted.</param>
    /// <returns>A builder for the API resource.</returns>
    /// <remarks>
    /// Create backends with <see cref="AddAzureOpenAIBackend"/> or <see cref="AddFoundryBackend"/>,
    /// compose them with <see cref="AddBackendPool"/>, and attach the pool with <see cref="WithBackend(IResourceBuilder{AzureApiManagementApiResource}, IResourceBuilder{AzureApiManagementBackendPoolResource})"/>.
    /// Requests beneath <paramref name="path"/> are appended to the selected backend URI.
    /// </remarks>
    [AspireExport]
    public static IResourceBuilder<AzureApiManagementApiResource> AddOpenAIApi(
        this IResourceBuilder<AzureApiManagementResource> builder,
        [ResourceName] string name,
        string path,
        string? displayName = null,
        bool subscriptionRequired = true,
        string? apiName = null)
    {
        var apiBuilder = AddApiCore(builder, name, path, displayName, subscriptionRequired, apiName);
 
        apiBuilder.AddOperation(
            CreateBoundedIdentifier($"{name}-chat-completions", 64),
            method: "POST",
            urlTemplate: "/chat/completions",
            displayName: "Create chat completion",
            operationName: "chat-completions");
 
        return apiBuilder;
    }
 
    /// <summary>
    /// Adds a product to the API Management service.
    /// </summary>
    /// <param name="builder">The Azure API Management resource builder.</param>
    /// <param name="name">The globally unique Aspire resource name and default physical product name.</param>
    /// <param name="displayName">The product display name.</param>
    /// <param name="options">The product options.</param>
    /// <param name="productName">The physical product identifier in API Management. The resource name is used when omitted.</param>
    /// <returns>A builder for the product resource.</returns>
    [AspireExport]
    public static IResourceBuilder<AzureApiManagementProductResource> AddProduct(
        this IResourceBuilder<AzureApiManagementResource> builder,
        [ResourceName] string name,
        string displayName,
        AzureApiManagementProductOptions? options = null,
        string? productName = null)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(name);
        ArgumentException.ThrowIfNullOrEmpty(displayName);
 
        var resolvedProductName = productName ?? name;
        ValidateGeneralIdentifier(resolvedProductName, 256, nameof(productName));
        if (displayName.Length > 300)
        {
            throw new ArgumentException("The product display name cannot exceed 300 characters.", nameof(displayName));
        }
 
        if (builder.Resource.Products.Any(product =>
            string.Equals(product.ProductName, resolvedProductName, StringComparison.OrdinalIgnoreCase)))
        {
            throw new InvalidOperationException($"An API Management product with physical name '{resolvedProductName}' has already been added.");
        }
 
        options ??= new AzureApiManagementProductOptions();
        if (options.Description?.Length > 1000)
        {
            throw new ArgumentException("The product description cannot exceed 1000 characters.", nameof(options));
        }
        if (options.ApprovalRequired && !options.SubscriptionRequired)
        {
            throw new ArgumentException("Product approval can only be required when subscriptions are required.", nameof(options));
        }
        if (options.SubscriptionsLimit is not null && !options.SubscriptionRequired)
        {
            throw new ArgumentException("A product subscription limit can only be configured when subscriptions are required.", nameof(options));
        }
        if (options.SubscriptionsLimit is <= 0)
        {
            throw new ArgumentOutOfRangeException(
                nameof(options),
                options.SubscriptionsLimit,
                "The product subscription limit must be greater than zero.");
        }
 
        var resource = new AzureApiManagementProductResource(
            name,
            resolvedProductName,
            displayName,
            options,
            builder.Resource);
        builder.Resource.Products.Add(resource);
 
        var resourceBuilder = builder.ApplicationBuilder.ExecutionContext.IsRunMode
            ? builder.ApplicationBuilder.CreateResourceBuilder(resource)
            : builder.ApplicationBuilder.AddResource(resource);
 
        return resourceBuilder
            .WithParentRelationship(builder.Resource)
            .ExcludeFromManifest()
            .WithIconName("BoxMultiple");
    }
 
    /// <summary>
    /// Adds an API to an API Management product.
    /// </summary>
    /// <param name="builder">The API Management product resource builder.</param>
    /// <param name="api">The API to add to the product.</param>
    /// <returns>The product resource builder.</returns>
    [AspireExport]
    public static IResourceBuilder<AzureApiManagementProductResource> WithApi(
        this IResourceBuilder<AzureApiManagementProductResource> builder,
        IResourceBuilder<AzureApiManagementApiResource> api)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(api);
 
        if (!ReferenceEquals(builder.Resource.Parent, api.Resource.Parent))
        {
            throw new InvalidOperationException("An API Management product can only contain APIs from the same API Management service.");
        }
        if (!builder.Resource.Apis.Contains(api.Resource))
        {
            builder.Resource.Apis.Add(api.Resource);
        }
 
        return builder.WithRelationship(api.Resource, "Includes");
    }
 
    /// <summary>
    /// Adds a subscription scoped to an API Management product.
    /// </summary>
    /// <param name="builder">The API Management product resource builder.</param>
    /// <param name="name">The globally unique Aspire resource name and default physical subscription name.</param>
    /// <param name="displayName">The subscription display name.</param>
    /// <param name="options">The subscription options.</param>
    /// <param name="subscriptionName">The physical subscription identifier in API Management. The resource name is used when omitted.</param>
    /// <returns>A builder for the subscription resource.</returns>
    /// <remarks>API Management generates the primary and secondary subscription keys during deployment.</remarks>
    [AspireExport]
    public static IResourceBuilder<AzureApiManagementSubscriptionResource> AddSubscription(
        this IResourceBuilder<AzureApiManagementProductResource> builder,
        [ResourceName] string name,
        string displayName,
        AzureApiManagementSubscriptionOptions? options = null,
        string? subscriptionName = null)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(name);
        ArgumentException.ThrowIfNullOrEmpty(displayName);
 
        var resolvedSubscriptionName = subscriptionName ?? name;
        ValidateGeneralIdentifier(resolvedSubscriptionName, 256, nameof(subscriptionName));
        if (displayName.Length > 100)
        {
            throw new ArgumentException("The subscription display name cannot exceed 100 characters.", nameof(displayName));
        }
 
        if (builder.Resource.Parent.Products
            .SelectMany(product => product.Subscriptions)
            .Any(subscription => string.Equals(
                subscription.SubscriptionName,
                resolvedSubscriptionName,
                StringComparison.OrdinalIgnoreCase)))
        {
            throw new InvalidOperationException($"An API Management subscription with physical name '{resolvedSubscriptionName}' has already been added.");
        }
 
        var resource = new AzureApiManagementSubscriptionResource(
            name,
            resolvedSubscriptionName,
            displayName,
            options ?? new AzureApiManagementSubscriptionOptions(),
            builder.Resource);
        builder.Resource.Subscriptions.Add(resource);
 
        var resourceBuilder = builder.ApplicationBuilder.ExecutionContext.IsRunMode
            ? builder.ApplicationBuilder.CreateResourceBuilder(resource)
            : builder.ApplicationBuilder.AddResource(resource);
 
        return resourceBuilder
            .WithParentRelationship(builder.Resource)
            .ExcludeFromManifest()
            .WithIconName("Key");
    }
 
    /// <summary>
    /// Adds a non-secret named value to the API Management service.
    /// </summary>
    /// <param name="builder">The Azure API Management resource builder.</param>
    /// <param name="name">The globally unique Aspire resource name and default physical named-value name.</param>
    /// <param name="value">The named value.</param>
    /// <param name="displayName">The name used to reference the value in policies. The resource name is used when omitted.</param>
    /// <param name="namedValueName">The physical named-value identifier in API Management. The resource name is used when omitted.</param>
    /// <param name="tags">Tags used to organize the named value.</param>
    /// <returns>A builder for the named-value resource.</returns>
    [AspireExport("addApiManagementNamedValue", MethodName = "addNamedValue")]
    public static IResourceBuilder<AzureApiManagementNamedValueResource> AddNamedValue(
        this IResourceBuilder<AzureApiManagementResource> builder,
        [ResourceName] string name,
        string value,
        string? displayName = null,
        string? namedValueName = null,
        string[]? tags = null)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(value);
        ValidateMaximumLength(value, 4096, "The named value", nameof(value));
        return AddNamedValueCore(builder, name, value, secret: false, displayName, namedValueName, tags);
    }
 
    /// <summary>
    /// Adds a secret parameter as a named value in the API Management service.
    /// </summary>
    /// <param name="builder">The Azure API Management resource builder.</param>
    /// <param name="name">The globally unique Aspire resource name and default physical named-value name.</param>
    /// <param name="value">The secret parameter containing the named value.</param>
    /// <param name="displayName">The name used to reference the value in policies. The resource name is used when omitted.</param>
    /// <param name="namedValueName">The physical named-value identifier in API Management. The resource name is used when omitted.</param>
    /// <param name="tags">Tags used to organize the named value.</param>
    /// <returns>A builder for the named-value resource.</returns>
    [AspireExport("addApiManagementSecretNamedValue", MethodName = "addSecretNamedValue")]
    public static IResourceBuilder<AzureApiManagementNamedValueResource> AddSecretNamedValue(
        this IResourceBuilder<AzureApiManagementResource> builder,
        [ResourceName] string name,
        IResourceBuilder<ParameterResource> value,
        string? displayName = null,
        string? namedValueName = null,
        string[]? tags = null)
    {
        ArgumentNullException.ThrowIfNull(value);
        if (!value.Resource.Secret)
        {
            throw new ArgumentException(
                "The named-value parameter must be marked as secret. Use AddParameter with secret: true when creating the parameter.",
                nameof(value));
        }
 
        return AddNamedValueCore(builder, name, value.Resource, secret: true, displayName, namedValueName, tags);
    }
 
    /// <summary>
    /// Adds a Key Vault-backed named value to the API Management service.
    /// </summary>
    /// <param name="builder">The Azure API Management resource builder.</param>
    /// <param name="name">The globally unique Aspire resource name and default physical named-value name.</param>
    /// <param name="value">The Key Vault secret reference.</param>
    /// <param name="displayName">The name used to reference the value in policies. The resource name is used when omitted.</param>
    /// <param name="namedValueName">The physical named-value identifier in API Management. The resource name is used when omitted.</param>
    /// <param name="tags">Tags used to organize the named value.</param>
    /// <returns>A builder for the named-value resource.</returns>
    [AspireExportIgnore(Reason = "Polyglot AppHosts pass the Key Vault resource and secret name separately.")]
    public static IResourceBuilder<AzureApiManagementNamedValueResource> AddKeyVaultNamedValue(
        this IResourceBuilder<AzureApiManagementResource> builder,
        [ResourceName] string name,
        IAzureKeyVaultSecretReference value,
        string? displayName = null,
        string? namedValueName = null,
        string[]? tags = null)
    {
        ArgumentNullException.ThrowIfNull(value);
        var namedValue = AddNamedValueCore(builder, name, value, secret: true, displayName, namedValueName, tags)
            .WithRelationship(value.Resource, "Secret");
        ConfigureKeyVaultAccess(builder.Resource, value, KeyVaultBuiltInRole.KeyVaultSecretsUser);
 
        return namedValue;
    }
 
    /// <summary>
    /// Adds a Key Vault-backed named value to the API Management service from a polyglot AppHost.
    /// </summary>
    [AspireExport("addApiManagementKeyVaultNamedValue", MethodName = "addKeyVaultNamedValue")]
    internal static IResourceBuilder<AzureApiManagementNamedValueResource> AddKeyVaultNamedValueForPolyglot(
        this IResourceBuilder<AzureApiManagementResource> builder,
        [ResourceName] string name,
        IResourceBuilder<AzureKeyVaultResource> vault,
        string secretName,
        string? displayName = null,
        string? namedValueName = null,
        string[]? tags = null)
    {
        ArgumentNullException.ThrowIfNull(vault);
 
        return builder.AddKeyVaultNamedValue(
            name,
            vault.GetSecret(secretName),
            displayName,
            namedValueName,
            tags);
    }
 
    /// <summary>
    /// Adds a reusable policy fragment to the API Management service.
    /// </summary>
    /// <param name="builder">The Azure API Management resource builder.</param>
    /// <param name="name">The globally unique Aspire resource name and default physical policy-fragment name.</param>
    /// <param name="policyXml">One or more well-formed APIM policy elements without a <c>fragment</c> envelope.</param>
    /// <param name="description">An optional description.</param>
    /// <param name="fragmentName">The physical policy-fragment identifier in API Management. The resource name is used when omitted.</param>
    /// <returns>A builder for the policy-fragment resource.</returns>
    [AspireExport]
    public static IResourceBuilder<AzureApiManagementPolicyFragmentResource> AddPolicyFragment(
        this IResourceBuilder<AzureApiManagementResource> builder,
        [ResourceName] string name,
        string policyXml,
        string? description = null,
        string? fragmentName = null)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(name);
        ValidatePolicyFragment(policyXml);
 
        var resolvedFragmentName = fragmentName ?? name;
        ValidatePolicyFragmentIdentifier(resolvedFragmentName, nameof(fragmentName));
        if (description?.Length > 1000)
        {
            throw new ArgumentException("The policy-fragment description cannot exceed 1000 characters.", nameof(description));
        }
        if (builder.Resource.PolicyFragments.Any(fragment =>
            string.Equals(fragment.FragmentName, resolvedFragmentName, StringComparison.OrdinalIgnoreCase)))
        {
            throw new InvalidOperationException($"An API Management policy fragment with physical name '{resolvedFragmentName}' has already been added.");
        }
 
        var resource = new AzureApiManagementPolicyFragmentResource(
            name,
            resolvedFragmentName,
            policyXml,
            description,
            builder.Resource);
        builder.Resource.PolicyFragments.Add(resource);
 
        var resourceBuilder = builder.ApplicationBuilder.ExecutionContext.IsRunMode
            ? builder.ApplicationBuilder.CreateResourceBuilder(resource)
            : builder.ApplicationBuilder.AddResource(resource);
 
        return resourceBuilder
            .WithParentRelationship(builder.Resource)
            .ExcludeFromManifest()
            .WithIconName("Code");
    }
 
    /// <summary>
    /// Includes a policy fragment in the service-level inbound policy.
    /// </summary>
    /// <param name="builder">The Azure API Management resource builder.</param>
    /// <param name="fragment">The policy fragment to include.</param>
    /// <returns>The resource builder.</returns>
    [AspireExport("withApiManagementServiceInboundPolicyFragment", MethodName = "withInboundPolicyFragment")]
    public static IResourceBuilder<AzureApiManagementResource> WithInboundPolicyFragment(
        this IResourceBuilder<AzureApiManagementResource> builder,
        IResourceBuilder<AzureApiManagementPolicyFragmentResource> fragment)
    {
        ValidatePolicyFragmentParent(builder.Resource, fragment);
        builder.WithInboundPolicy(CreateIncludeFragmentPolicy(fragment.Resource.FragmentName));
        return builder.WithRelationship(fragment.Resource, "Policy fragment");
    }
 
    /// <summary>
    /// Includes a policy fragment in the API-level inbound policy.
    /// </summary>
    /// <param name="builder">The API Management API resource builder.</param>
    /// <param name="fragment">The policy fragment to include.</param>
    /// <returns>The resource builder.</returns>
    [AspireExport("withApiManagementApiInboundPolicyFragment", MethodName = "withInboundPolicyFragment")]
    public static IResourceBuilder<AzureApiManagementApiResource> WithInboundPolicyFragment(
        this IResourceBuilder<AzureApiManagementApiResource> builder,
        IResourceBuilder<AzureApiManagementPolicyFragmentResource> fragment)
    {
        ValidatePolicyFragmentParent(builder.Resource.Parent, fragment);
        builder.WithInboundPolicy(CreateIncludeFragmentPolicy(fragment.Resource.FragmentName));
        return builder.WithRelationship(fragment.Resource, "Policy fragment");
    }
 
    /// <summary>
    /// Includes a policy fragment in the operation-level inbound policy.
    /// </summary>
    /// <param name="builder">The API Management operation resource builder.</param>
    /// <param name="fragment">The policy fragment to include.</param>
    /// <returns>The resource builder.</returns>
    [AspireExport("withApiManagementOperationInboundPolicyFragment", MethodName = "withInboundPolicyFragment")]
    public static IResourceBuilder<AzureApiManagementOperationResource> WithInboundPolicyFragment(
        this IResourceBuilder<AzureApiManagementOperationResource> builder,
        IResourceBuilder<AzureApiManagementPolicyFragmentResource> fragment)
    {
        ValidatePolicyFragmentParent(builder.Resource.Parent.Parent, fragment);
        builder.WithInboundPolicy(CreateIncludeFragmentPolicy(fragment.Resource.FragmentName));
        return builder.WithRelationship(fragment.Resource, "Policy fragment");
    }
 
    /// <summary>
    /// Sends service-level API Management diagnostics to Application Insights.
    /// </summary>
    /// <param name="builder">The Azure API Management resource builder.</param>
    /// <param name="applicationInsights">The Application Insights resource.</param>
    /// <param name="options">The diagnostic options.</param>
    /// <returns>The resource builder.</returns>
    [AspireExport("withApiManagementServiceApplicationInsights", MethodName = "withApplicationInsights")]
    public static IResourceBuilder<AzureApiManagementResource> WithApplicationInsights(
        this IResourceBuilder<AzureApiManagementResource> builder,
        IResourceBuilder<AzureApplicationInsightsResource> applicationInsights,
        AzureApiManagementDiagnosticOptions? options = null)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(applicationInsights);
        if (builder.Resource.Diagnostic is not null)
        {
            throw new InvalidOperationException("Application Insights diagnostics have already been configured for this API Management service.");
        }
 
        options ??= new AzureApiManagementDiagnosticOptions();
        ValidateDiagnosticOptions(options);
        builder.Resource.Diagnostic = new(applicationInsights.Resource, options);
        return builder.WithRelationship(applicationInsights.Resource, "Diagnostics");
    }
 
    /// <summary>
    /// Sends API-level API Management diagnostics to Application Insights.
    /// </summary>
    /// <param name="builder">The API Management API resource builder.</param>
    /// <param name="applicationInsights">The Application Insights resource.</param>
    /// <param name="options">The diagnostic options.</param>
    /// <returns>The resource builder.</returns>
    [AspireExport("withApiManagementApiApplicationInsights", MethodName = "withApplicationInsights")]
    public static IResourceBuilder<AzureApiManagementApiResource> WithApplicationInsights(
        this IResourceBuilder<AzureApiManagementApiResource> builder,
        IResourceBuilder<AzureApplicationInsightsResource> applicationInsights,
        AzureApiManagementDiagnosticOptions? options = null)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(applicationInsights);
        if (builder.Resource.Diagnostic is not null)
        {
            throw new InvalidOperationException("Application Insights diagnostics have already been configured for this API Management API.");
        }
 
        options ??= new AzureApiManagementDiagnosticOptions();
        ValidateDiagnosticOptions(options);
        builder.Resource.Diagnostic = new(applicationInsights.Resource, options);
        return builder.WithRelationship(applicationInsights.Resource, "Diagnostics");
    }
 
    /// <summary>
    /// Configures a custom hostname using a certificate stored in Azure Key Vault.
    /// </summary>
    /// <param name="builder">The Azure API Management resource builder.</param>
    /// <param name="hostname">The fully qualified custom hostname.</param>
    /// <param name="certificate">The Key Vault secret containing a PFX certificate.</param>
    /// <param name="type">The API Management endpoint to configure.</param>
    /// <param name="defaultSslBinding">Whether this certificate is the default SNI fallback. This is valid only for gateway hostnames.</param>
    /// <param name="negotiateClientCertificate">Whether the endpoint negotiates client certificates.</param>
    /// <returns>The resource builder.</returns>
    [AspireExportIgnore(Reason = "Polyglot AppHosts pass the Key Vault resource and secret name separately.")]
    public static IResourceBuilder<AzureApiManagementResource> WithCustomDomain(
        this IResourceBuilder<AzureApiManagementResource> builder,
        string hostname,
        IAzureKeyVaultSecretReference certificate,
        AzureApiManagementHostnameType type = AzureApiManagementHostnameType.Proxy,
        bool defaultSslBinding = false,
        bool negotiateClientCertificate = false)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(hostname);
        ArgumentNullException.ThrowIfNull(certificate);
 
        if (Uri.CheckHostName(hostname) != UriHostNameType.Dns ||
            !hostname.TrimEnd('.').Contains('.'))
        {
            throw new ArgumentException("The custom hostname must be a fully qualified DNS name.", nameof(hostname));
        }
        if (defaultSslBinding && type != AzureApiManagementHostnameType.Proxy)
        {
            throw new ArgumentException("The default SSL binding can only be configured for a proxy hostname.", nameof(defaultSslBinding));
        }
        if (defaultSslBinding && builder.Resource.CustomDomains.Any(domain => domain.DefaultSslBinding))
        {
            throw new InvalidOperationException("Only one custom hostname can be configured as the default SSL binding.");
        }
        if (builder.Resource.Options.Sku == AzureApiManagementSku.Consumption &&
            type != AzureApiManagementHostnameType.Proxy)
        {
            throw new InvalidOperationException(
                "The API Management Consumption SKU supports custom domains only for the gateway endpoint.");
        }
        if (builder.Resource.Options.Sku is AzureApiManagementSku.BasicV2 or AzureApiManagementSku.StandardV2 or AzureApiManagementSku.PremiumV2 &&
            type is AzureApiManagementHostnameType.Management or AzureApiManagementHostnameType.Scm)
        {
            throw new InvalidOperationException(
                $"Custom domains for the '{type}' endpoint are not supported by API Management v2 SKUs.");
        }
        if (type == AzureApiManagementHostnameType.Proxy &&
            builder.Resource.CustomDomains.Any(domain => domain.Type == AzureApiManagementHostnameType.Proxy) &&
            builder.Resource.Options.Sku is not (AzureApiManagementSku.Developer or AzureApiManagementSku.Premium or AzureApiManagementSku.PremiumV2))
        {
            throw new InvalidOperationException(
                $"Multiple gateway custom domains are not supported by the API Management '{builder.Resource.Options.Sku}' SKU.");
        }
        if (builder.Resource.CustomDomains.Any(domain =>
            domain.Type == type &&
            string.Equals(domain.Hostname, hostname, StringComparison.OrdinalIgnoreCase)))
        {
            throw new InvalidOperationException($"The custom hostname '{hostname}' has already been configured for endpoint type '{type}'.");
        }
        if (type != AzureApiManagementHostnameType.Proxy &&
            builder.Resource.CustomDomains.Any(domain => domain.Type == type))
        {
            throw new InvalidOperationException($"Only one custom hostname can be configured for endpoint type '{type}'.");
        }
 
        builder.Resource.CustomDomains.Add(new(
            hostname,
            certificate,
            type,
            defaultSslBinding,
            negotiateClientCertificate));
        ConfigureKeyVaultAccess(builder.Resource, certificate, KeyVaultBuiltInRole.KeyVaultCertificateUser);
 
        return builder.WithRelationship(certificate.Resource, "Certificate");
    }
 
    /// <summary>
    /// Configures a custom hostname from a polyglot AppHost using a certificate stored in Azure Key Vault.
    /// </summary>
    [AspireExport("withCustomDomain")]
    internal static IResourceBuilder<AzureApiManagementResource> WithCustomDomainForPolyglot(
        this IResourceBuilder<AzureApiManagementResource> builder,
        string hostname,
        IResourceBuilder<AzureKeyVaultResource> vault,
        string certificateSecretName,
        AzureApiManagementHostnameType type = AzureApiManagementHostnameType.Proxy,
        bool defaultSslBinding = false,
        bool negotiateClientCertificate = false)
    {
        ArgumentNullException.ThrowIfNull(vault);
 
        return builder.WithCustomDomain(
            hostname,
            vault.GetSecret(certificateSecretName),
            type,
            defaultSslBinding,
            negotiateClientCertificate);
    }
 
    /// <summary>
    /// Adds a configurable backend to the API Management service.
    /// </summary>
    /// <param name="builder">The API Management resource builder.</param>
    /// <param name="name">The globally unique Aspire resource name and default physical backend name.</param>
    /// <param name="uri">The deferred URI of the backend service.</param>
    /// <param name="options">The backend options.</param>
    /// <param name="backendName">The physical backend identifier in API Management. The Aspire resource name is used when omitted.</param>
    /// <returns>A builder for the backend resource.</returns>
    [AspireExport]
    public static IResourceBuilder<AzureApiManagementBackendResource> AddBackend(
        this IResourceBuilder<AzureApiManagementResource> builder,
        [ResourceName] string name,
        ReferenceExpression uri,
        AzureApiManagementBackendOptions? options = null,
        string? backendName = null)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(name);
        ArgumentNullException.ThrowIfNull(uri);
 
        var resolvedBackendName = backendName ?? name;
        ValidateGeneralIdentifier(resolvedBackendName, 80, nameof(backendName));
        if (builder.Resource.Backends.Any(backend =>
            string.Equals(backend.BackendName, resolvedBackendName, StringComparison.OrdinalIgnoreCase)) ||
            builder.Resource.BackendPools.Any(pool =>
                string.Equals(pool.BackendPoolName, resolvedBackendName, StringComparison.OrdinalIgnoreCase)))
        {
            throw new InvalidOperationException(
                $"An API Management backend with physical name '{resolvedBackendName}' has already been added.");
        }
 
        options ??= new AzureApiManagementBackendOptions();
        ValidateBackendOptions(builder.Resource, options);
        ValidateDisplayName(options.Title ?? name, "backend title", nameof(options));
 
        var resource = new AzureApiManagementBackendResource(name, resolvedBackendName, uri, options, builder.Resource);
        builder.Resource.Backends.Add(resource);
 
        return (builder.ApplicationBuilder.ExecutionContext.IsRunMode
                ? builder.ApplicationBuilder.CreateResourceBuilder(resource)
                : builder.ApplicationBuilder.AddResource(resource))
            .WithParentRelationship(builder.Resource)
            .ExcludeFromManifest()
            .WithIconName("ArrowRouting");
    }
 
    /// <summary>
    /// Adds an Azure OpenAI deployment as an API Management backend.
    /// </summary>
    [AspireExport]
    public static IResourceBuilder<AzureApiManagementBackendResource> AddAzureOpenAIBackend(
        this IResourceBuilder<AzureApiManagementResource> builder,
        [ResourceName] string name,
        IResourceBuilder<AzureOpenAIDeploymentResource> deployment,
        string? backendName = null)
    {
        ArgumentNullException.ThrowIfNull(deployment);
 
        var backend = AddOpenAIBackend(
            builder,
            name,
            deployment.Resource.Parent,
            ReferenceExpression.Create($"{deployment.Resource.Parent.Endpoint}"),
            deployment.Resource.DeploymentName,
            backendName);
        builder.Resource.AddIdentityRoleAssignment(
            deployment.Resource.Parent,
            new RoleDefinition(
                CognitiveServicesBuiltInRole.CognitiveServicesOpenAIUser.ToString(),
                CognitiveServicesBuiltInRole.GetBuiltInRoleName(CognitiveServicesBuiltInRole.CognitiveServicesOpenAIUser)));
 
        return backend.WithRelationship(deployment.Resource, "Backend");
    }
 
    /// <summary>
    /// Adds a Microsoft Foundry OpenAI deployment as an API Management backend.
    /// </summary>
    [AspireExport]
    public static IResourceBuilder<AzureApiManagementBackendResource> AddFoundryBackend(
        this IResourceBuilder<AzureApiManagementResource> builder,
        [ResourceName] string name,
        IResourceBuilder<FoundryDeploymentResource> deployment,
        string? backendName = null)
    {
        ArgumentNullException.ThrowIfNull(deployment);
        if (!string.Equals(deployment.Resource.Format, "OpenAI", StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException(
               $"Foundry deployment '{deployment.Resource.Name}' uses format '{deployment.Resource.Format}'. " +
               "Only OpenAI-format deployments can be added as a Foundry backend.");
        }
 
        var backend = AddOpenAIBackend(
            builder,
            name,
            deployment.Resource.Parent,
            ReferenceExpression.Create($"{deployment.Resource.Parent.Endpoint}"),
            deployment.Resource.DeploymentName,
            backendName);
        builder.Resource.AddIdentityRoleAssignment(
            deployment.Resource.Parent,
            new RoleDefinition(
                CognitiveServicesBuiltInRole.CognitiveServicesUser.ToString(),
                CognitiveServicesBuiltInRole.GetBuiltInRoleName(CognitiveServicesBuiltInRole.CognitiveServicesUser)));
 
        return backend.WithRelationship(deployment.Resource, "Backend");
    }
 
    /// <summary>
    /// Adds an Azure Blob Storage service as an API Management backend.
    /// </summary>
    [AspireExport]
    public static IResourceBuilder<AzureApiManagementBackendResource> AddBlobStorageBackend(
        this IResourceBuilder<AzureApiManagementResource> builder,
        [ResourceName] string name,
        IResourceBuilder<AzureBlobStorageResource> blobs,
        string? backendName = null)
    {
        ArgumentNullException.ThrowIfNull(blobs);
 
        var backend = builder.AddBackend(
            name,
            blobs.Resource.UriExpression,
            new AzureApiManagementBackendOptions
            {
                ManagedIdentityResource = "https://storage.azure.com/",
            },
            backendName);
        builder.Resource.AddIdentityRoleAssignment(
            blobs.Resource.Parent,
            new RoleDefinition(
                StorageBuiltInRole.StorageBlobDataReader.ToString(),
                StorageBuiltInRole.GetBuiltInRoleName(StorageBuiltInRole.StorageBlobDataReader)));
 
        return backend.WithRelationship(blobs.Resource, "Backend");
    }
 
    /// <summary>
    /// Adds a reusable load-balancing pool to the API Management service.
    /// </summary>
    [AspireExport]
    public static IResourceBuilder<AzureApiManagementBackendPoolResource> AddBackendPool(
        this IResourceBuilder<AzureApiManagementResource> builder,
        [ResourceName] string name,
        string? displayName = null,
        string? backendPoolName = null)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(name);
 
        var resolvedBackendPoolName = backendPoolName ?? name;
        ValidateGeneralIdentifier(resolvedBackendPoolName, 80, nameof(backendPoolName));
        if (builder.Resource.BackendPools.Any(pool =>
            string.Equals(pool.BackendPoolName, resolvedBackendPoolName, StringComparison.OrdinalIgnoreCase)) ||
            builder.Resource.Backends.Any(backend =>
                string.Equals(backend.BackendName, resolvedBackendPoolName, StringComparison.OrdinalIgnoreCase)))
        {
            throw new InvalidOperationException(
                $"An API Management backend or backend pool with physical name '{resolvedBackendPoolName}' has already been added.");
        }
 
        var resource = new AzureApiManagementBackendPoolResource(
            name,
            resolvedBackendPoolName,
            displayName ?? name,
            builder.Resource);
        ValidateDisplayName(resource.DisplayName, "backend-pool display name", nameof(displayName));
        builder.Resource.BackendPools.Add(resource);
 
        return (builder.ApplicationBuilder.ExecutionContext.IsRunMode
                ? builder.ApplicationBuilder.CreateResourceBuilder(resource)
                : builder.ApplicationBuilder.AddResource(resource))
            .WithParentRelationship(builder.Resource)
            .ExcludeFromManifest()
            .WithIconName("ArrowRouting");
    }
 
    /// <summary>
    /// Adds a backend to a load-balancing pool.
    /// </summary>
    [AspireExport("addBackendPoolMember")]
    public static IResourceBuilder<AzureApiManagementBackendPoolResource> WithBackend(
        this IResourceBuilder<AzureApiManagementBackendPoolResource> builder,
        IResourceBuilder<AzureApiManagementBackendResource> backend,
        int priority = 1,
        int weight = 1)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(backend);
        if (!ReferenceEquals(builder.Resource.Parent, backend.Resource.Parent))
        {
            throw new InvalidOperationException(
                $"Backend '{backend.Resource.Name}' and pool '{builder.Resource.Name}' must belong to the same API Management service.");
        }
        if (priority is < 0 or > 100)
        {
            throw new ArgumentOutOfRangeException(nameof(priority), priority, "Priority must be between 0 and 100.");
        }
        if (weight is < 0 or > 100)
        {
            throw new ArgumentOutOfRangeException(nameof(weight), weight, "Weight must be between 0 and 100.");
        }
        if (builder.Resource.Backends.Count == 30)
        {
            throw new InvalidOperationException("An API Management backend pool cannot contain more than 30 backends.");
        }
        if (builder.Resource.Backends.Any(member => ReferenceEquals(member.Backend, backend.Resource)))
        {
            throw new InvalidOperationException(
                $"Backend '{backend.Resource.Name}' has already been added to pool '{builder.Resource.Name}'.");
        }
 
        var existingAudience = builder.Resource.Backends
            .Select(member => member.Backend.Options.ManagedIdentityResource)
            .FirstOrDefault();
        if (builder.Resource.Backends.Count > 0 &&
            !string.Equals(existingAudience, backend.Resource.Options.ManagedIdentityResource, StringComparison.Ordinal))
        {
            throw new InvalidOperationException(
                $"All backends in pool '{builder.Resource.Name}' must use the same managed-identity resource URI.");
        }
 
        builder.Resource.Backends.Add(new(backend.Resource, priority, weight));
        return builder.WithRelationship(backend.Resource, "Member");
    }
 
    /// <summary>
    /// Configures an API to route requests to a backend.
    /// </summary>
    [AspireExport("withApiBackend")]
    public static IResourceBuilder<AzureApiManagementApiResource> WithBackend(
        this IResourceBuilder<AzureApiManagementApiResource> builder,
        IResourceBuilder<AzureApiManagementBackendResource> backend)
    {
        return WithBackendCore(builder, backend.Resource);
    }
 
    /// <summary>
    /// Configures an API to route requests to a backend pool.
    /// </summary>
    [AspireExport("withApiBackendPool")]
    public static IResourceBuilder<AzureApiManagementApiResource> WithBackend(
        this IResourceBuilder<AzureApiManagementApiResource> builder,
        IResourceBuilder<AzureApiManagementBackendPoolResource> backend)
    {
        return WithBackendCore(builder, backend.Resource);
    }
 
    private static IResourceBuilder<AzureApiManagementApiResource> AddApiCore(
        IResourceBuilder<AzureApiManagementResource> builder,
        string name,
        string path,
        string? displayName,
        bool subscriptionRequired,
        string? apiName,
        IComputeResource? target = null)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(name);
        ArgumentException.ThrowIfNullOrEmpty(path);
        ValidateApiIdentifier(apiName ?? name, nameof(apiName));
 
        var normalizedPath = path.Trim('/');
        ArgumentException.ThrowIfNullOrEmpty(normalizedPath);
        ValidateMaximumLength(normalizedPath, 400, "The API path", nameof(path));
        ValidateDisplayName(displayName ?? name, "API display name", nameof(displayName));
        ValidateApiUniqueness(builder.Resource, apiName ?? name, normalizedPath);
 
        var resource = target is null
            ? new AzureApiManagementApiResource(
                name,
                apiName ?? name,
                normalizedPath,
                displayName ?? name,
                subscriptionRequired,
                builder.Resource)
            : new AzureApiManagementApiResource(
                name,
                apiName ?? name,
                normalizedPath,
                displayName ?? name,
                subscriptionRequired,
                target,
                builder.Resource);
        builder.Resource.Apis.Add(resource);
 
        return (builder.ApplicationBuilder.ExecutionContext.IsRunMode
                ? builder.ApplicationBuilder.CreateResourceBuilder(resource)
                : builder.ApplicationBuilder.AddResource(resource))
            .WithParentRelationship(builder.Resource)
            .ExcludeFromManifest()
            .WithIconName("DocumentTableArrowRight");
    }
 
    private static IResourceBuilder<AzureApiManagementBackendResource> AddOpenAIBackend(
        IResourceBuilder<AzureApiManagementResource> builder,
        string name,
        AzureProvisioningResource account,
        ReferenceExpression endpoint,
        string deploymentName,
        string? backendName)
    {
        if (builder.Resource.Options.Sku == AzureApiManagementSku.Consumption)
        {
            throw new InvalidOperationException(
                "OpenAI backends require API Management backend circuit breakers, which are not supported by the Consumption SKU.");
        }
 
        var backend = builder.AddBackend(
            name,
            ReferenceExpression.Create($"{endpoint}openai/deployments/{deploymentName}"),
            new AzureApiManagementBackendOptions
            {
                ManagedIdentityResource = "https://cognitiveservices.azure.com",
                CircuitBreaker = new AzureApiManagementCircuitBreakerOptions
                {
                    Name = "openAIThrottling",
                    StatusCodeRanges = [new(429, 429)],
                    AcceptRetryAfter = true,
                },
            },
            backendName);
 
        return backend.WithRelationship(account, "Account");
    }
 
    private static IResourceBuilder<AzureApiManagementApiResource> WithBackendCore(
        IResourceBuilder<AzureApiManagementApiResource> builder,
        IResource backend)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(backend);
        if (builder.Resource.Target is not null)
        {
            throw new InvalidOperationException(
                $"API '{builder.Resource.Name}' already targets compute resource '{builder.Resource.Target.Name}'.");
        }
 
        var backendParent = backend switch
        {
            AzureApiManagementBackendResource value => value.Parent,
            AzureApiManagementBackendPoolResource value => value.Parent,
            _ => throw new ArgumentException("The resource must be an API Management backend or backend pool.", nameof(backend)),
        };
        if (!ReferenceEquals(builder.Resource.Parent, backendParent))
        {
            throw new InvalidOperationException(
                $"API '{builder.Resource.Name}' and backend '{backend.Name}' must belong to the same API Management service.");
        }
        if (builder.Resource.Backend is not null)
        {
            throw new InvalidOperationException($"API '{builder.Resource.Name}' already has a backend.");
        }
 
        builder.Resource.Backend = backend;
        return builder.WithRelationship(backend, "Backend");
    }
 
    /// <summary>
    /// Adds an explicitly modeled operation to an API.
    /// </summary>
    /// <param name="builder">The API resource builder.</param>
    /// <param name="name">The globally unique Aspire resource name and default physical operation name.</param>
    /// <param name="method">The HTTP method.</param>
    /// <param name="urlTemplate">The URL template relative to the API path.</param>
    /// <param name="displayName">The operation display name. The resource name is used when omitted.</param>
    /// <param name="operationName">The physical operation identifier in API Management. The Aspire resource name is used when omitted.</param>
    /// <returns>A builder for the operation resource.</returns>
    /// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> is null.</exception>
    /// <exception cref="ArgumentException">Thrown when a required string is empty.</exception>
    /// <exception cref="InvalidOperationException">Thrown when the operation identifier or route duplicates another operation or conflicts with a generated proxy route.</exception>
    [AspireExport]
    public static IResourceBuilder<AzureApiManagementOperationResource> AddOperation(
        this IResourceBuilder<AzureApiManagementApiResource> builder,
        [ResourceName] string name,
        string method,
        string urlTemplate,
        string? displayName = null,
        string? operationName = null)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(name);
        ArgumentException.ThrowIfNullOrEmpty(method);
        ArgumentException.ThrowIfNullOrEmpty(urlTemplate);
        ValidateMaximumLength(urlTemplate, 1000, "The operation URL template", nameof(urlTemplate));
        ValidateDisplayName(displayName ?? name, "operation display name", nameof(displayName));
        var physicalOperationName = operationName ?? name;
        ValidateOperationIdentifier(physicalOperationName, nameof(operationName));
 
        if (string.Equals(physicalOperationName, ProxyOperationName, StringComparison.OrdinalIgnoreCase) ||
            s_proxyOperationMethods.Any(proxyMethod =>
                string.Equals(physicalOperationName, GetProxyOperationName(proxyMethod), StringComparison.OrdinalIgnoreCase) ||
                string.Equals(physicalOperationName, GetRootOperationName(proxyMethod), StringComparison.OrdinalIgnoreCase)))
        {
            throw new ArgumentException(
                $"The API Management operation identifier '{physicalOperationName}' is reserved for a generated catch-all operation.",
                nameof(operationName));
        }
 
        if (string.Equals(method, "*", StringComparison.Ordinal))
        {
            throw new ArgumentException(
                "API Management does not reliably dispatch operations that use the wildcard HTTP method. Specify a concrete HTTP method instead.",
                nameof(method));
        }
 
        if (builder.Resource.Operations.Any(
            operation => string.Equals(operation.OperationName, physicalOperationName, StringComparison.OrdinalIgnoreCase)))
        {
            throw new InvalidOperationException(
                $"API '{builder.Resource.Name}' already contains an operation with the physical identifier '{physicalOperationName}'.");
        }
 
        var signature = GetOperationSignature(method, urlTemplate);
        if (builder.Resource.Operations.Any(operation =>
            string.Equals(GetOperationSignature(operation.Method, operation.UrlTemplate), signature, StringComparison.OrdinalIgnoreCase)))
        {
            throw new InvalidOperationException(
                $"API '{builder.Resource.Name}' already contains an operation with the route '{method.ToUpperInvariant()} {urlTemplate}'. " +
                "HTTP method and URL template must be unique, regardless of parameter names.");
        }
 
        var resource = new AzureApiManagementOperationResource(
            name,
            physicalOperationName,
            method.ToUpperInvariant(),
            urlTemplate,
            displayName ?? name,
            builder.Resource);
 
        builder.Resource.Operations.Add(resource);
 
        var resourceBuilder = builder.ApplicationBuilder.ExecutionContext.IsRunMode
            ? builder.ApplicationBuilder.CreateResourceBuilder(resource)
            : builder.ApplicationBuilder.AddResource(resource);
 
        return resourceBuilder
            .WithParentRelationship(builder.Resource)
            .ExcludeFromManifest()
            .WithIconName("DocumentTableArrowRight");
    }
 
    /// <summary>
    /// Appends an XML policy statement to the service-level inbound policy section.
    /// </summary>
    /// <param name="builder">The Azure API Management resource builder.</param>
    /// <param name="policyXml">One or more well-formed APIM policy elements without a <c>policies</c> envelope.</param>
    /// <returns>The resource builder.</returns>
    /// <exception cref="ArgumentException">Thrown when <paramref name="policyXml"/> is empty or is not a valid XML fragment.</exception>
    [AspireExport("withApiManagementServiceInboundPolicy", MethodName = "withInboundPolicy")]
    public static IResourceBuilder<AzureApiManagementResource> WithInboundPolicy(
        this IResourceBuilder<AzureApiManagementResource> builder,
        string policyXml)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ValidatePolicyFragment(policyXml);
        EnsurePolicyFragmentCanBeAdded(builder.Resource.PolicyXml, "service");
        builder.Resource.AddInboundPolicyStatement(policyXml);
        return builder;
    }
 
    /// <summary>
    /// Replaces the complete service-level policy document.
    /// </summary>
    /// <param name="builder">The Azure API Management resource builder.</param>
    /// <param name="policyXml">A complete APIM <c>policies</c> XML document.</param>
    /// <returns>The resource builder.</returns>
    /// <remarks>
    /// APIM policy updates replace the complete policy document at a scope. To prevent silently dropping policy fragments,
    /// this method cannot be combined with <see cref="WithInboundPolicy(IResourceBuilder{AzureApiManagementResource}, string)"/>
    /// at the same scope.
    /// </remarks>
    /// <exception cref="ArgumentException">Thrown when <paramref name="policyXml"/> is not a complete policy document.</exception>
    [AspireExport("withApiManagementServicePolicy", MethodName = "withPolicy")]
    public static IResourceBuilder<AzureApiManagementResource> WithPolicy(
        this IResourceBuilder<AzureApiManagementResource> builder,
        string policyXml)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ValidatePolicyDocument(policyXml);
        EnsureCompletePolicyCanBeSet(builder.Resource.InboundPolicyStatements, "service");
        builder.Resource.PolicyXml = policyXml;
        return builder;
    }
 
    /// <summary>
    /// Appends an XML policy statement to the API-level inbound policy section.
    /// </summary>
    /// <param name="builder">The API resource builder.</param>
    /// <param name="policyXml">One or more well-formed APIM policy elements without a <c>policies</c> envelope.</param>
    /// <returns>The resource builder.</returns>
    /// <exception cref="ArgumentException">Thrown when <paramref name="policyXml"/> is empty or is not a valid XML fragment.</exception>
    [AspireExport("withApiManagementApiInboundPolicy", MethodName = "withInboundPolicy")]
    public static IResourceBuilder<AzureApiManagementApiResource> WithInboundPolicy(
        this IResourceBuilder<AzureApiManagementApiResource> builder,
        string policyXml)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ValidatePolicyFragment(policyXml);
        EnsurePolicyFragmentCanBeAdded(builder.Resource.PolicyXml, "API");
        builder.Resource.AddInboundPolicyStatement(policyXml);
        return builder;
    }
 
    /// <summary>
    /// Replaces the complete API-level policy document.
    /// </summary>
    /// <param name="builder">The API resource builder.</param>
    /// <param name="policyXml">A complete APIM <c>policies</c> XML document.</param>
    /// <returns>The resource builder.</returns>
    /// <remarks>
    /// Replacing the API policy also replaces the generated backend-routing policy. The supplied document must
    /// configure backend routing when the API should continue forwarding requests.
    /// </remarks>
    /// <exception cref="ArgumentException">Thrown when <paramref name="policyXml"/> is not a complete policy document.</exception>
    [AspireExport("withApiManagementApiPolicy", MethodName = "withPolicy")]
    public static IResourceBuilder<AzureApiManagementApiResource> WithPolicy(
        this IResourceBuilder<AzureApiManagementApiResource> builder,
        string policyXml)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ValidatePolicyDocument(policyXml);
        EnsureCompletePolicyCanBeSet(builder.Resource.InboundPolicyStatements, "API");
        builder.Resource.PolicyXml = policyXml;
        return builder;
    }
 
    /// <summary>
    /// Appends an XML policy statement to the operation-level inbound policy section.
    /// </summary>
    /// <param name="builder">The operation resource builder.</param>
    /// <param name="policyXml">One or more well-formed APIM policy elements without a <c>policies</c> envelope.</param>
    /// <returns>The resource builder.</returns>
    /// <exception cref="ArgumentException">Thrown when <paramref name="policyXml"/> is empty or is not a valid XML fragment.</exception>
    [AspireExport("withApiManagementOperationInboundPolicy", MethodName = "withInboundPolicy")]
    public static IResourceBuilder<AzureApiManagementOperationResource> WithInboundPolicy(
        this IResourceBuilder<AzureApiManagementOperationResource> builder,
        string policyXml)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ValidatePolicyFragment(policyXml);
        EnsurePolicyFragmentCanBeAdded(builder.Resource.PolicyXml, "operation");
        builder.Resource.AddInboundPolicyStatement(policyXml);
        return builder;
    }
 
    /// <summary>
    /// Replaces the complete operation-level policy document.
    /// </summary>
    /// <param name="builder">The operation resource builder.</param>
    /// <param name="policyXml">A complete APIM <c>policies</c> XML document.</param>
    /// <returns>The resource builder.</returns>
    /// <exception cref="ArgumentException">Thrown when <paramref name="policyXml"/> is not a complete policy document.</exception>
    [AspireExport("withApiManagementOperationPolicy", MethodName = "withPolicy")]
    public static IResourceBuilder<AzureApiManagementOperationResource> WithPolicy(
        this IResourceBuilder<AzureApiManagementOperationResource> builder,
        string policyXml)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ValidatePolicyDocument(policyXml);
        EnsureCompletePolicyCanBeSet(builder.Resource.InboundPolicyStatements, "operation");
        builder.Resource.PolicyXml = policyXml;
        return builder;
    }
 
    /// <summary>
    /// Injects a classic Developer or Premium API Management service into a virtual network subnet.
    /// </summary>
    /// <param name="builder">The Azure API Management resource builder.</param>
    /// <param name="subnet">The subnet resource. Classic APIM injection requires an undelegated subnet with an NSG.</param>
    /// <param name="mode">Whether the APIM endpoints are externally or internally accessible.</param>
    /// <returns>The resource builder.</returns>
    /// <remarks>
    /// This API models classic VNet injection only. Standard v2 outbound integration and Premium v2 injection have
    /// different subnet delegations and lifecycle constraints. Use <see cref="WithVirtualNetworkIntegration"/> for
    /// Standard v2 outbound integration or <see cref="WithVirtualNetworkInjection"/> for Premium v2 injection.
    /// See <see href="https://learn.microsoft.com/azure/api-management/virtual-network-concepts">APIM virtual networking</see>.
    /// </remarks>
    /// <exception cref="InvalidOperationException">Thrown when the selected SKU does not support classic VNet injection or the subnet is delegated.</exception>
    [AspireExport]
    public static IResourceBuilder<AzureApiManagementResource> WithClassicVirtualNetwork(
        this IResourceBuilder<AzureApiManagementResource> builder,
        IResourceBuilder<AzureSubnetResource> subnet,
        AzureApiManagementVirtualNetworkMode mode)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(subnet);
 
        if (builder.Resource.Options.Sku is not (AzureApiManagementSku.Developer or AzureApiManagementSku.Premium))
        {
            throw new InvalidOperationException(
                $"Classic virtual network injection is supported only by the Developer and Premium SKUs, not '{builder.Resource.Options.Sku}'.");
        }
 
        if (subnet.Resource.Annotations.OfType<AzureSubnetServiceDelegationAnnotation>().Any())
        {
            throw new InvalidOperationException(
                $"Subnet '{subnet.Resource.Name}' is delegated to another Azure service. Classic API Management injection requires an undelegated subnet.");
        }
 
        ValidateSubnetOwnership(builder.Resource, subnet.Resource);
        if (SetVirtualNetworkConfiguration(
                builder.Resource,
                subnet.Resource,
                mode,
                AzureApiManagementVirtualNetworkKind.Classic))
        {
            subnet.Resource.Annotations.Add(new AzureApiManagementSubnetUsageAnnotation(builder.Resource));
        }
        return builder.WithRelationship(subnet.Resource, "Virtual network");
    }
 
    /// <summary>
    /// Integrates a Standard v2 or Premium v2 API Management service with a virtual network for outbound access.
    /// </summary>
    /// <param name="builder">The Azure API Management resource builder.</param>
    /// <param name="subnet">A dedicated subnet for API Management virtual network integration.</param>
    /// <returns>The resource builder.</returns>
    /// <remarks>
    /// The subnet is delegated to <c>Microsoft.Web/serverFarms</c>. The APIM gateway remains publicly accessible,
    /// while backend traffic can reach resources in the virtual network.
    /// See <see href="https://learn.microsoft.com/azure/api-management/integrate-vnet-outbound">APIM outbound virtual network integration</see>.
    /// </remarks>
    /// <exception cref="InvalidOperationException">
    /// Thrown when the selected SKU does not support v2 virtual network integration, the subnet has a conflicting
    /// delegation, the subnet is already used by another API Management service, or its NSG cannot
    /// accommodate the required Key Vault allow rule ahead of outbound deny rules.
    /// </exception>
    [AspireExport]
    public static IResourceBuilder<AzureApiManagementResource> WithVirtualNetworkIntegration(
        this IResourceBuilder<AzureApiManagementResource> builder,
        IResourceBuilder<AzureSubnetResource> subnet)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(subnet);
 
        if (builder.Resource.Options.Sku is not (AzureApiManagementSku.StandardV2 or AzureApiManagementSku.PremiumV2))
        {
            throw new InvalidOperationException(
                $"Virtual network integration is supported only by the Standard v2 and Premium v2 SKUs, not '{builder.Resource.Options.Sku}'.");
        }
 
        ValidateV2SubnetSize(subnet.Resource);
        ValidateSubnetOwnership(builder.Resource, subnet.Resource);
        if (builder.Resource.VirtualNetworkConfiguration is null)
        {
            _ = GetKeyVaultRulePriority(subnet.Resource);
        }
        ConfigureV2SubnetDelegation(subnet, AzureSubnetServiceDelegations.AppServiceEnvironments);
        if (SetVirtualNetworkConfiguration(
                builder.Resource,
                subnet.Resource,
                AzureApiManagementVirtualNetworkMode.External,
                AzureApiManagementVirtualNetworkKind.V2Integration))
        {
            subnet.Resource.Annotations.Add(new AzureApiManagementSubnetUsageAnnotation(builder.Resource));
            ConfigureV2SubnetNetworkSecurityGroup(subnet);
        }
 
        return builder.WithRelationship(subnet.Resource, "Virtual network integration");
    }
 
    /// <summary>
    /// Injects a Premium v2 API Management service into a virtual network for private inbound and outbound access.
    /// </summary>
    /// <param name="builder">The Azure API Management resource builder.</param>
    /// <param name="subnet">A dedicated subnet for API Management virtual network injection.</param>
    /// <returns>The resource builder.</returns>
    /// <remarks>
    /// The subnet is delegated to <c>Microsoft.Web/hostingEnvironments</c>. Premium v2 injection can only be
    /// selected when the service is created and cannot later be changed to virtual network integration.
    /// See <see href="https://learn.microsoft.com/azure/api-management/inject-vnet-v2">Premium v2 virtual network injection</see>.
    /// </remarks>
    /// <exception cref="InvalidOperationException">
    /// Thrown when the selected SKU is not Premium v2, the subnet has a conflicting delegation, or the subnet is
    /// already used by another API Management service, or its NSG cannot accommodate the required
    /// Key Vault allow rule ahead of outbound deny rules.
    /// </exception>
    [AspireExport]
    public static IResourceBuilder<AzureApiManagementResource> WithVirtualNetworkInjection(
        this IResourceBuilder<AzureApiManagementResource> builder,
        IResourceBuilder<AzureSubnetResource> subnet)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentNullException.ThrowIfNull(subnet);
 
        if (builder.Resource.Options.Sku is not AzureApiManagementSku.PremiumV2)
        {
            throw new InvalidOperationException(
                $"Virtual network injection is supported only by the Premium v2 SKU, not '{builder.Resource.Options.Sku}'.");
        }
 
        ValidateV2SubnetSize(subnet.Resource);
        ValidateSubnetOwnership(builder.Resource, subnet.Resource);
        if (builder.Resource.VirtualNetworkConfiguration is null)
        {
            _ = GetKeyVaultRulePriority(subnet.Resource);
        }
        ConfigureV2SubnetDelegation(subnet, PremiumV2SubnetDelegation);
        if (SetVirtualNetworkConfiguration(
                builder.Resource,
                subnet.Resource,
                AzureApiManagementVirtualNetworkMode.Internal,
                AzureApiManagementVirtualNetworkKind.PremiumV2Injection))
        {
            subnet.Resource.Annotations.Add(new AzureApiManagementSubnetUsageAnnotation(builder.Resource));
            ConfigureV2SubnetNetworkSecurityGroup(subnet);
        }
 
        return builder.WithRelationship(subnet.Resource, "Virtual network injection");
    }
 
    private static void ConfigureInfrastructure(AzureResourceInfrastructure infrastructure)
    {
        var azureResource = (AzureApiManagementResource)infrastructure.AspireResource;
 
        ValidateBackendPhysicalNames(azureResource);
 
        if (azureResource.VirtualNetworkConfiguration is { } virtualNetworkConfiguration)
        {
            ValidateFinalSubnetDelegation(virtualNetworkConfiguration);
        }
 
        if (azureResource.VirtualNetworkConfiguration is
            { Kind: AzureApiManagementVirtualNetworkKind.V2Integration or AzureApiManagementVirtualNetworkKind.PremiumV2Injection } network)
        {
            var rules = network.Subnet.NetworkSecurityGroup?.SecurityRules;
            var allowRule = rules?.SingleOrDefault(rule => string.Equals(rule.Name, "allow-apim-key-vault", StringComparison.OrdinalIgnoreCase));
            if (rules is null || allowRule is null || allowRule.Access != SecurityRuleAccess.Allow ||
                allowRule.Direction != SecurityRuleDirection.Outbound ||
                allowRule.Protocol != SecurityRuleProtocol.Tcp ||
                allowRule.DestinationPortRange != "443" ||
                allowRule.SourcePortRange != "*" ||
                allowRule.SourceAddressPrefix != AzureServiceTags.VirtualNetwork ||
                allowRule.DestinationAddressPrefix != AzureServiceTags.AzureKeyVault ||
                allowRule.SourceAddressPrefixReference is not null ||
                allowRule.DestinationAddressPrefixReference is not null ||
                rules.Any(rule => CouldDenyKeyVault(rule) && rule.Priority <= allowRule.Priority))
            {
                throw new InvalidOperationException(
                    $"Subnet '{network.Subnet.Name}' must retain an effective 'allow-apim-key-vault' NSG rule ahead of outbound TCP/443 deny rules.");
            }
        }
 
        var isExisting = azureResource.IsExisting();
        if (isExisting && !azureResource.Options.HasExplicitSku)
        {
            throw new InvalidOperationException(
                $"Existing API Management resource '{azureResource.Name}' requires an explicit {nameof(AzureApiManagementOptions.Sku)} " +
                "matching the adopted service. The default Developer SKU is only used for new services.");
        }
 
        if (isExisting &&
            !azureResource.Identity.IsExisting() &&
            (azureResource.Backends.Any(backend => backend.Options.ManagedIdentityResource is not null) ||
             azureResource.NamedValues.Any(namedValue => namedValue.Value is IAzureKeyVaultSecretReference)))
        {
            throw new InvalidOperationException(
                $"Existing API Management resource '{azureResource.Name}' uses managed-identity backends or Key Vault-backed named values, " +
                "but its user-assigned identity is not configured as an existing resource. " +
                $"Supply an already-attached identity using {nameof(WithAzureUserAssignedIdentity)}(...) and configure it with PublishAsExisting(...). " +
                "Aspire does not attach identities to existing services.");
        }
 
        var hasExplicitExistingScope =
            azureResource.TryGetLastAnnotation<ExistingAzureResourceAnnotation>(out var existingAnnotation) &&
            (existingAnnotation.ResourceGroup is not null || existingAnnotation.Subscription is not null);
        var hasPrivateEndpoint = azureResource.HasAnnotationOfType<PrivateEndpointTargetAnnotation>();
 
        if (hasPrivateEndpoint)
        {
            if (azureResource.Options.Sku is AzureApiManagementSku.Consumption or AzureApiManagementSku.BasicV2)
            {
                throw new InvalidOperationException(
                    $"API Management SKU '{azureResource.Options.Sku}' does not support private endpoints.");
            }
 
            if (azureResource.VirtualNetworkConfiguration is
                {
                    Kind: AzureApiManagementVirtualNetworkKind.Classic or
                        AzureApiManagementVirtualNetworkKind.PremiumV2Injection,
                })
            {
                throw new InvalidOperationException(
                    $"API Management resource '{azureResource.Name}' cannot combine a private endpoint with virtual network injection.");
            }
        }
 
        if (isExisting && azureResource.VirtualNetworkConfiguration is not null)
        {
            throw new InvalidOperationException(
                $"Existing API Management resource '{azureResource.Name}' cannot change its virtual network configuration.");
        }
 
        if (isExisting && azureResource.CustomDomains.Count > 0)
        {
            throw new InvalidOperationException(
                $"Existing API Management resource '{azureResource.Name}' cannot change its custom domains.");
        }
 
        if (isExisting &&
            hasExplicitExistingScope &&
            (azureResource.Diagnostic is not null ||
             azureResource.Apis.Any(api => api.Diagnostic is not null)))
        {
            throw new InvalidOperationException(
                $"Existing API Management resource '{azureResource.Name}' cannot configure diagnostics " +
                "when it is adopted from another resource group or subscription.");
        }
 
        var service = isExisting
            ? AzureProvisioningResource.CreateExistingOrNewProvisionableResource(
                infrastructure,
                static (identifier, name) =>
                {
                    var existing = ApiManagementService.FromExisting(identifier, "2025-03-01-preview");
                    existing.Name = name;
                    return existing;
                },
                static _ => throw new UnreachableException())
            // Premium v2 injection requires this preview ARM version, even though the SDK defaults to 2024-05-01.
            // https://learn.microsoft.com/azure/api-management/inject-vnet-v2
            : new ApiManagementService(infrastructure.AspireResource.GetBicepIdentifier(), "2025-03-01-preview")
            {
                PublisherEmail = azureResource.Options.PublisherEmail,
                PublisherName = azureResource.Options.PublisherName,
                Sku = new ApiManagementServiceSkuProperties
                {
                    Name = GetProvisioningSku(azureResource.Options.Sku),
                    Capacity = azureResource.Options.Capacity,
                },
                Identity = new ManagedServiceIdentity
                {
                    ManagedServiceIdentityType = ManagedServiceIdentityType.UserAssigned,
                    UserAssignedIdentities =
                    {
                        [BicepFunction.Interpolate($"{azureResource.Identity.Id.AsProvisioningParameter(infrastructure)}").Compile().ToString()] =
                            new UserAssignedIdentityDetails(),
                    },
                },
                VirtualNetworkType = VirtualNetworkType.None,
                Tags =
                {
                    { "aspire-resource-name", azureResource.Name },
                },
            };
 
        if (azureResource.VirtualNetworkConfiguration is { } virtualNetwork)
        {
            service.VirtualNetworkType = virtualNetwork.Mode switch
            {
                AzureApiManagementVirtualNetworkMode.External => VirtualNetworkType.External,
                AzureApiManagementVirtualNetworkMode.Internal => VirtualNetworkType.Internal,
                _ => throw new UnreachableException(),
            };
            service.VirtualNetworkConfiguration = new VirtualNetworkConfiguration
            {
                SubnetResourceId = virtualNetwork.Subnet.Id.AsProvisioningParameter(infrastructure),
            };
        }
 
        var keyVaultRoleAssignments = new Dictionary<string, RoleAssignment>(StringComparer.Ordinal);
        foreach (var customDomain in azureResource.CustomDomains)
        {
            var secret = customDomain.Certificate.AsKeyVaultSecret(infrastructure);
            if (customDomain.Certificate.Resource is not AzureProvisioningResource)
            {
                var roleAssignment = AddKeyVaultRoleAssignment(
                    infrastructure,
                    customDomain.Certificate,
                    (UserAssignedIdentity)azureResource.Identity.AddAsExistingResource(infrastructure),
                    KeyVaultBuiltInRole.KeyVaultCertificateUser,
                    keyVaultRoleAssignments);
                service.DependsOn.Add(roleAssignment);
            }
            service.HostnameConfigurations.Add(new HostnameConfiguration
            {
                HostnameType = GetProvisioningHostnameType(customDomain.Type),
                HostName = customDomain.Hostname,
                KeyVaultSecretUri = new BicepValue<Uri>(CreateVersionlessSecretUri(secret).Compile()),
                IdentityClientId = azureResource.Identity.ClientId.AsProvisioningParameter(infrastructure),
                IsDefaultSslBindingEnabled = customDomain.DefaultSslBinding,
                IsClientCertificateNegotiationEnabled = customDomain.NegotiateClientCertificate,
            });
        }
 
        if (!isExisting)
        {
            infrastructure.Add(service);
        }
 
        var namedValues = AddNamedValues(
            infrastructure,
            azureResource,
            service,
            keyVaultRoleAssignments);
        var policyFragments = AddPolicyFragments(infrastructure, azureResource, service, namedValues);
        AddServicePolicy(infrastructure, azureResource, service, policyFragments, namedValues);
 
        var provisionedBackends = AddBackends(infrastructure, azureResource, service);
        var provisionedBackendPools = AddBackendPools(infrastructure, azureResource, service, provisionedBackends);
 
        var provisionedApis = new Dictionary<AzureApiManagementApiResource, ApiManagementApi>();
        foreach (var apiResource in azureResource.Apis)
        {
            provisionedApis.Add(
                apiResource,
                AddApi(
                    infrastructure,
                    apiResource,
                    service,
                    provisionedBackends,
                    provisionedBackendPools,
                    policyFragments,
                    namedValues));
        }
 
        AddProducts(infrastructure, azureResource, service, provisionedApis);
        AddDiagnostics(infrastructure, azureResource, service, provisionedApis);
 
        infrastructure.Add(new ProvisioningOutput("gatewayUrl", typeof(string))
        {
            Value = service.GatewayUri,
        });
        infrastructure.Add(new ProvisioningOutput("name", typeof(string))
        {
            Value = service.Name,
        });
        infrastructure.Add(new ProvisioningOutput("id", typeof(string))
        {
            Value = service.Id,
        });
    }
 
    private static void AddServicePolicy(
        AzureResourceInfrastructure infrastructure,
        AzureApiManagementResource azureResource,
        ApiManagementService service,
        IReadOnlyList<PolicyFragmentContract> policyFragments,
        IReadOnlyList<ApiManagementNamedValue> namedValues)
    {
        var policyXml = azureResource.PolicyXml ??
            CreatePolicyDocument(azureResource.InboundPolicyStatements, inheritParentPolicy: false);
 
        if (policyXml is null)
        {
            return;
        }
 
        var policy = new ApiManagementPolicy(
            CreateGeneratedBicepIdentifier("servicePolicy", service.BicepIdentifier))
        {
            Parent = service,
            Name = "policy",
            Format = PolicyContentFormat.RawXml,
            Value = policyXml!,
        };
        foreach (var policyFragment in policyFragments)
        {
            policy.DependsOn.Add(policyFragment);
        }
        foreach (var namedValue in namedValues)
        {
            policy.DependsOn.Add(namedValue);
        }
        infrastructure.Add(policy);
    }
 
    private static IReadOnlyList<PolicyFragmentContract> AddPolicyFragments(
        AzureResourceInfrastructure infrastructure,
        AzureApiManagementResource azureResource,
        ApiManagementService service,
        IReadOnlyList<ApiManagementNamedValue> namedValues)
    {
        var provisionedFragments = new List<PolicyFragmentContract>();
        foreach (var fragmentResource in azureResource.PolicyFragments)
        {
            var fragment = new PolicyFragmentContract(
                Infrastructure.NormalizeBicepIdentifier(fragmentResource.Name))
            {
                Parent = service,
                Name = fragmentResource.FragmentName,
                Format = PolicyFragmentContentFormat.Rawxml,
                Value = CreatePolicyFragmentDocument(fragmentResource.Value),
            };
            if (fragmentResource.Description is not null)
            {
                fragment.Description = fragmentResource.Description;
            }
            foreach (var namedValue in namedValues)
            {
                fragment.DependsOn.Add(namedValue);
            }
 
            infrastructure.Add(fragment);
            provisionedFragments.Add(fragment);
        }
 
        return provisionedFragments;
    }
 
    private static IReadOnlyList<ApiManagementNamedValue> AddNamedValues(
        AzureResourceInfrastructure infrastructure,
        AzureApiManagementResource azureResource,
        ApiManagementService service,
        Dictionary<string, RoleAssignment> keyVaultRoleAssignments)
    {
        var provisionedNamedValues = new List<ApiManagementNamedValue>();
        foreach (var namedValueResource in azureResource.NamedValues)
        {
            var namedValue = new ApiManagementNamedValue(
                Infrastructure.NormalizeBicepIdentifier(namedValueResource.Name))
            {
                Parent = service,
                Name = namedValueResource.NamedValueName,
                DisplayName = namedValueResource.DisplayName,
                IsSecret = namedValueResource.Secret,
            };
            foreach (var tag in namedValueResource.Tags)
            {
                namedValue.Tags.Add(tag);
            }
 
            if (namedValueResource.Value is IAzureKeyVaultSecretReference secretReference)
            {
                var secret = secretReference.AsKeyVaultSecret(infrastructure);
                namedValue.KeyVault = new KeyVaultContractCreateProperties
                {
                    SecretIdentifier = CreateVersionlessSecretUri(secret),
                    IdentityClientId = azureResource.Identity.ClientId.AsProvisioningParameter(infrastructure),
                };
                if (secretReference.Resource is not AzureProvisioningResource)
                {
                    var roleAssignment = AddKeyVaultRoleAssignment(
                        infrastructure,
                        secretReference,
                        (UserAssignedIdentity)azureResource.Identity.AddAsExistingResource(infrastructure),
                        KeyVaultBuiltInRole.KeyVaultSecretsUser,
                        keyVaultRoleAssignments);
                    namedValue.DependsOn.Add(roleAssignment);
                }
            }
            else if (namedValueResource.Value is ParameterResource parameter)
            {
                namedValue.Value = parameter.AsProvisioningParameter(
                    infrastructure,
                    parameterName: CreateGeneratedBicepIdentifier("namedValueParameter", namedValueResource.Name),
                    isSecure: true);
            }
            else
            {
                namedValue.Value = (string)namedValueResource.Value;
            }
 
            infrastructure.Add(namedValue);
            provisionedNamedValues.Add(namedValue);
        }
 
        return provisionedNamedValues;
    }
 
    private static void AddProducts(
        AzureResourceInfrastructure infrastructure,
        AzureApiManagementResource azureResource,
        ApiManagementService service,
        IReadOnlyDictionary<AzureApiManagementApiResource, ApiManagementApi> provisionedApis)
    {
        foreach (var productResource in azureResource.Products)
        {
            var productIdentifier = Infrastructure.NormalizeBicepIdentifier(productResource.Name);
            var product = new ApiManagementProduct(productIdentifier)
            {
                Parent = service,
                Name = productResource.ProductName,
                DisplayName = productResource.DisplayName,
                IsSubscriptionRequired = productResource.Options.SubscriptionRequired,
                IsApprovalRequired = productResource.Options.ApprovalRequired,
                State = productResource.Options.State switch
                {
                    AzureApiManagementProductState.NotPublished => ApiManagementProductState.NotPublished,
                    AzureApiManagementProductState.Published => ApiManagementProductState.Published,
                    _ => throw new UnreachableException(),
                },
            };
            if (productResource.Options.SubscriptionsLimit is { } subscriptionsLimit)
            {
                product.SubscriptionsLimit = subscriptionsLimit;
            }
            if (productResource.Options.Description is not null)
            {
                product.Description = productResource.Options.Description;
            }
            if (productResource.Options.Terms is not null)
            {
                product.Terms = productResource.Options.Terms;
            }
            infrastructure.Add(product);
 
            foreach (var apiResource in productResource.Apis)
            {
                var productApi = new ServiceProductApiLink(
                    CreateGeneratedBicepIdentifier("productApi", productResource.Name, apiResource.Name))
                {
                    Parent = product,
                    Name = CreateBoundedIdentifier(apiResource.ApiName, 80),
                    ApiId = new BicepValue<string>(provisionedApis[apiResource].Id.Compile()),
                };
                productApi.DependsOn.Add(provisionedApis[apiResource]);
                infrastructure.Add(productApi);
            }
 
            foreach (var subscriptionResource in productResource.Subscriptions)
            {
                var subscription = new ApiManagementSubscription(
                    Infrastructure.NormalizeBicepIdentifier(subscriptionResource.Name))
                {
                    Parent = service,
                    Name = subscriptionResource.SubscriptionName,
                    DisplayName = subscriptionResource.DisplayName,
                    Scope = new BicepValue<string>(product.Id.Compile()),
                    State = subscriptionResource.Options.State switch
                    {
                        AzureApiManagementSubscriptionState.Active => SubscriptionState.Active,
                        AzureApiManagementSubscriptionState.Suspended => SubscriptionState.Suspended,
                        AzureApiManagementSubscriptionState.Submitted => SubscriptionState.Submitted,
                        AzureApiManagementSubscriptionState.Rejected => SubscriptionState.Rejected,
                        AzureApiManagementSubscriptionState.Expired => SubscriptionState.Expired,
                        AzureApiManagementSubscriptionState.Cancelled => SubscriptionState.Cancelled,
                        _ => throw new UnreachableException(),
                    },
                    AllowTracing = subscriptionResource.Options.AllowTracing,
                };
                infrastructure.Add(subscription);
            }
        }
    }
 
    private static void AddDiagnostics(
        AzureResourceInfrastructure infrastructure,
        AzureApiManagementResource azureResource,
        ApiManagementService service,
        IReadOnlyDictionary<AzureApiManagementApiResource, ApiManagementApi> provisionedApis)
    {
        var configuredDiagnostics = azureResource.Apis
            .Where(api => api.Diagnostic is not null)
            .Select(api => api.Diagnostic!)
            .Prepend(azureResource.Diagnostic)
            .OfType<AzureApiManagementDiagnostic>()
            .ToArray();
        if (configuredDiagnostics.Length == 0)
        {
            return;
        }
 
        var loggers = new Dictionary<AzureApplicationInsightsResource, ApiManagementLogger>();
        foreach (var diagnostic in configuredDiagnostics)
        {
            if (loggers.ContainsKey(diagnostic.ApplicationInsights))
            {
                continue;
            }
 
            var applicationInsights = (ApplicationInsightsComponent)diagnostic.ApplicationInsights.AddAsExistingResource(infrastructure);
            var logger = new ApiManagementLogger(
                CreateGeneratedBicepIdentifier("logger", diagnostic.ApplicationInsights.Name))
            {
                Parent = service,
                Name = CreateBoundedIdentifier($"{diagnostic.ApplicationInsights.Name}-application-insights", 256),
                LoggerType = LoggerType.ApplicationInsights,
                ResourceId = applicationInsights.Id,
                IsBuffered = true,
                Credentials =
                {
                    { "instrumentationKey", applicationInsights.InstrumentationKey },
                },
            };
            infrastructure.Add(logger);
            loggers.Add(diagnostic.ApplicationInsights, logger);
        }
 
        if (azureResource.Diagnostic is { } serviceDiagnostic)
        {
            var diagnostic = CreateServiceDiagnostic(
                service,
                loggers[serviceDiagnostic.ApplicationInsights],
                serviceDiagnostic.Options);
            infrastructure.Add(diagnostic);
        }
 
        foreach (var apiResource in azureResource.Apis)
        {
            if (apiResource.Diagnostic is not { } apiDiagnostic)
            {
                continue;
            }
 
            var diagnostic = CreateApiDiagnostic(
                provisionedApis[apiResource],
                loggers[apiDiagnostic.ApplicationInsights],
                apiDiagnostic.Options);
            infrastructure.Add(diagnostic);
        }
    }
 
    private static ApiManagementDiagnostic CreateServiceDiagnostic(
        ApiManagementService service,
        ApiManagementLogger logger,
        AzureApiManagementDiagnosticOptions options)
    {
        var diagnostic = new ApiManagementDiagnostic(
            CreateGeneratedBicepIdentifier("serviceDiagnostic", service.BicepIdentifier))
        {
            Parent = service,
            Name = "applicationinsights",
            LoggerId = new BicepValue<string>(logger.Id.Compile()),
            AlwaysLog = AlwaysLog.AllErrors,
            Sampling = new SamplingSettings
            {
                SamplingType = SamplingType.Fixed,
                Percentage = options.SamplingPercentage,
            },
            HttpCorrelationProtocol = HttpCorrelationProtocol.W3C,
            IsLogClientIPEnabled = options.LogClientIp,
            Verbosity = GetProvisioningVerbosity(options.Verbosity),
            OperationNameFormat = OperationNameFormat.Name,
            Metrics = true,
        };
        diagnostic.DependsOn.Add(logger);
        return diagnostic;
    }
 
    private static ApiDiagnostic CreateApiDiagnostic(
        ApiManagementApi api,
        ApiManagementLogger logger,
        AzureApiManagementDiagnosticOptions options)
    {
        var diagnostic = new ApiDiagnostic(
            CreateGeneratedBicepIdentifier("apiDiagnostic", api.BicepIdentifier))
        {
            Parent = api,
            Name = "applicationinsights",
            LoggerId = new BicepValue<string>(logger.Id.Compile()),
            AlwaysLog = AlwaysLog.AllErrors,
            Sampling = new SamplingSettings
            {
                SamplingType = SamplingType.Fixed,
                Percentage = options.SamplingPercentage,
            },
            HttpCorrelationProtocol = HttpCorrelationProtocol.W3C,
            IsLogClientIPEnabled = options.LogClientIp,
            Verbosity = GetProvisioningVerbosity(options.Verbosity),
            OperationNameFormat = OperationNameFormat.Name,
            Metrics = true,
        };
        diagnostic.DependsOn.Add(logger);
        return diagnostic;
    }
 
    private static void ConfigureKeyVaultAccess(
        AzureApiManagementResource apiManagement,
        IAzureKeyVaultSecretReference secret,
        KeyVaultBuiltInRole role)
    {
        if (secret.Resource is AzureProvisioningResource target)
        {
            apiManagement.AddIdentityRoleAssignment(
                target,
                new RoleDefinition(role.ToString(), KeyVaultBuiltInRole.GetBuiltInRoleName(role)));
        }
    }
 
    private static RoleAssignment AddKeyVaultRoleAssignment(
        AzureResourceInfrastructure infrastructure,
        IAzureKeyVaultSecretReference secretReference,
        UserAssignedIdentity identity,
        KeyVaultBuiltInRole role,
        Dictionary<string, RoleAssignment> roleAssignments)
    {
        var secret = secretReference.AsKeyVaultSecret(infrastructure);
        var vault = secret.Parent
            ?? throw new InvalidOperationException($"Key Vault secret '{secretReference.SecretName}' does not have a parent vault.");
        var key = $"{vault.BicepIdentifier}:{role}";
        if (roleAssignments.TryGetValue(key, out var existingRoleAssignment))
        {
            return existingRoleAssignment;
        }
 
        var roleDefinitionId = BicepFunction.GetSubscriptionResourceId(
            "Microsoft.Authorization/roleDefinitions",
            role.ToString());
        var roleAssignment = new RoleAssignment(
            Infrastructure.NormalizeBicepIdentifier($"{vault.BicepIdentifier}_{KeyVaultBuiltInRole.GetBuiltInRoleName(role)}"))
        {
            // Role assignment names must be known at the start of deployment. The principal ID is
            // runtime-only, so use the identity resource ID to keep the name stable and deterministic.
            Name = BicepFunction.CreateGuid(vault.Id, identity.Id, roleDefinitionId),
            Scope = new IdentifierExpression(vault.BicepIdentifier),
            PrincipalType = RoleManagementPrincipalType.ServicePrincipal,
            PrincipalId = identity.PrincipalId,
            RoleDefinitionId = roleDefinitionId,
        };
        infrastructure.Add(roleAssignment);
        roleAssignments.Add(key, roleAssignment);
        return roleAssignment;
    }
 
    private static BicepValue<string> CreateVersionlessSecretUri(KeyVaultSecret secret)
    {
        var vault = secret.Parent
            ?? throw new InvalidOperationException($"Key Vault secret '{secret.BicepIdentifier}' does not have a parent vault.");
 
        // APIM refreshes Key Vault-backed values and certificates only when the URI does not pin a secret version.
        // The generated URI has the form https://{vault}.vault.azure.net/secrets/{secret-name}.
        return BicepFunction.Interpolate($"{vault.Properties.VaultUri}secrets/{secret.Name}");
    }
 
    private static ApiManagementApi AddApi(
        AzureResourceInfrastructure infrastructure,
        AzureApiManagementApiResource apiResource,
        ApiManagementService service,
        IReadOnlyDictionary<AzureApiManagementBackendResource, ApiManagementBackend> provisionedBackends,
        IReadOnlyDictionary<AzureApiManagementBackendPoolResource, ApiManagementBackend> provisionedBackendPools,
        IReadOnlyList<PolicyFragmentContract> policyFragments,
        IReadOnlyList<ApiManagementNamedValue> namedValues)
    {
        var apiIdentifier = Infrastructure.NormalizeBicepIdentifier(apiResource.Name);
        var api = new ApiManagementApi(
            apiIdentifier)
        {
            Parent = service,
            Name = apiResource.ApiName,
            DisplayName = apiResource.DisplayName,
            Path = apiResource.Path,
            IsSubscriptionRequired = apiResource.SubscriptionRequired,
            ApiType = ApiType.Http,
            Protocols =
            {
                ApiOperationInvokableProtocol.Https,
            },
        };
 
        if (apiResource.OpenApiSource is AzureApiManagementOpenApiFile document)
        {
            var content = File.ReadAllText(document.Path);
            if (string.IsNullOrWhiteSpace(content))
            {
                throw new InvalidOperationException(
                    $"The OpenAPI document '{document.Path}' for API '{apiResource.Name}' is empty.");
            }
 
            api.Format = GetOpenApiImportFormat(document.Format, isLink: false);
            api.Value = content;
        }
        else if (apiResource.OpenApiSource is AzureApiManagementOpenApiEndpoint endpointSource)
        {
            var documentUrl = ResolveOpenApiEndpoint(apiResource, endpointSource);
            api.Format = GetOpenApiImportFormat(endpointSource.Format, isLink: true);
            api.Value = documentUrl.AsProvisioningParameter(
                infrastructure,
                CreateGeneratedBicepIdentifier("openApiUrl", apiIdentifier));
        }
 
        var usesImportedServiceUrl = apiResource.OpenApiSource is AzureApiManagementOpenApiEndpoint;
        if (usesImportedServiceUrl)
        {
            infrastructure.Add(api);
        }
 
        var (backendIdentifier, backend, managedIdentityResource) = apiResource.Target is not null
            ? AddComputeBackend(infrastructure, apiResource, api, service, apiIdentifier)
            : ResolveApiManagementBackend(apiResource, provisionedBackends, provisionedBackendPools);
 
        if (!usesImportedServiceUrl)
        {
            infrastructure.Add(api);
        }
 
        List<ProvisionableResource> operationDependencies = [];
        if (apiResource.OpenApiSource is null)
        {
            // Re-check explicit operations against the reserved proxy routes here, once the API's final
            // OpenAPI configuration is known, rather than at AddOperation call time. WithOpenApiDocument
            // can be applied after AddOperation, and call order must not change validation outcomes.
            foreach (var operationResource in apiResource.Operations)
            {
                var signature = GetOperationSignature(operationResource.Method, operationResource.UrlTemplate);
                if (s_proxyOperationMethods.Any(proxyMethod =>
                    string.Equals(signature, GetOperationSignature(proxyMethod, "/{*path}"), StringComparison.OrdinalIgnoreCase) ||
                    string.Equals(signature, GetOperationSignature(proxyMethod, "/"), StringComparison.OrdinalIgnoreCase)))
                {
                    throw new InvalidOperationException(
                        $"The route '{operationResource.Method} {operationResource.UrlTemplate}' conflicts with a generated proxy operation. " +
                        $"Configure an OpenAPI import for API '{apiResource.Name}' if you do not want the generated proxy routes.");
                }
            }
 
            // APIM's management plane accepts "*" as an operation method, but gateways do not
            // reliably dispatch it. Materialize the catch-all route for each supported method.
            foreach (var method in s_proxyOperationMethods)
            {
                // The wildcard matches descendants but not the API suffix itself (for example /api).
                var rootOperation = new ApiOperation(
                    CreateGeneratedBicepIdentifier($"proxy{method}RootOperation", apiIdentifier))
                {
                    Parent = api,
                    Name = GetRootOperationName(method),
                    DisplayName = $"Proxy {method} root",
                    Method = method,
                    UriTemplate = "/",
                };
                infrastructure.Add(rootOperation);
                operationDependencies.Add(rootOperation);
 
                var catchAllOperation = new ApiOperation(
                    CreateGeneratedBicepIdentifier($"proxy{method}Operation", apiIdentifier))
                {
                    Parent = api,
                    Name = GetProxyOperationName(method),
                    DisplayName = $"Proxy {method}",
                    Method = method,
                    UriTemplate = "/{*path}",
                };
                catchAllOperation.TemplateParameters.Add(new ParameterContract
                {
                    Name = "path",
                    ParameterContractType = "string",
                    IsRequired = true,
                });
                infrastructure.Add(catchAllOperation);
                operationDependencies.Add(catchAllOperation);
            }
        }
 
        foreach (var operationResource in apiResource.Operations)
        {
            operationDependencies.Add(AddOperation(infrastructure, operationResource, api, policyFragments, namedValues));
        }
 
        var policyXml = apiResource.PolicyXml ??
            CreatePolicyDocument(
                apiResource.InboundPolicyStatements,
                backendIdentifier,
                managedIdentityResource: managedIdentityResource,
                managedIdentityClientId: managedIdentityResource is not null
                    ? apiResource.Parent.Identity.ClientId.AsProvisioningParameter(infrastructure)
                    : (BicepValue<string>?)null);
 
        var policy = new ApiPolicy(
            CreateGeneratedBicepIdentifier("apiPolicy", apiIdentifier))
        {
            Parent = api,
            Name = "policy",
            Format = PolicyContentFormat.RawXml,
            Value = policyXml!,
        };
        policy.DependsOn.Add(backend);
        // The API policy write failed with PreconditionFailed while operation writes overlapped
        // in the APIM deployment test. Finish this API's operations and their policies first.
        foreach (var operationDependency in operationDependencies)
        {
            policy.DependsOn.Add(operationDependency);
        }
        foreach (var policyFragment in policyFragments)
        {
            policy.DependsOn.Add(policyFragment);
        }
        foreach (var namedValue in namedValues)
        {
            policy.DependsOn.Add(namedValue);
        }
        infrastructure.Add(policy);
 
        return api;
    }
 
    private static (string Identifier, ApiManagementBackend Backend, string? ManagedIdentityResource)
        AddComputeBackend(
            AzureResourceInfrastructure infrastructure,
            AzureApiManagementApiResource apiResource,
            ApiManagementApi api,
            ApiManagementService service,
            string apiIdentifier)
    {
        Debug.Assert(apiResource.Target is not null);
 
        if (apiResource.Target is not IResourceWithEndpoints endpointResource)
        {
            throw new InvalidOperationException(
                $"Resource '{apiResource.Target.Name}' does not expose endpoints and cannot be used as an API Management backend.");
        }
 
        var httpEndpoints = endpointResource.GetEndpoints()
            .Where(e => e.EndpointAnnotation.UriScheme is "http" or "https")
            .ToArray();
        var endpoint = httpEndpoints.FirstOrDefault(e => e.EndpointAnnotation.IsExternal)
            ?? httpEndpoints.FirstOrDefault()
            ?? throw new InvalidOperationException(
                $"Resource '{apiResource.Target.Name}' does not have an HTTP or HTTPS endpoint.");
 
        if (!endpoint.EndpointAnnotation.IsExternal && apiResource.Parent.VirtualNetworkConfiguration is null)
        {
            throw new InvalidOperationException(
                $"Resource '{apiResource.Target.Name}' exposes only internal HTTP endpoints. " +
                $"Configure virtual network connectivity for API Management resource '{apiResource.Parent.Name}' before adding it as a backend.");
        }
 
        if (!ComputeEnvironmentEndpointResolver.TryGetEffectiveComputeEnvironment(apiResource.Target, out var computeEnvironment))
        {
            throw new InvalidOperationException(
                $"Resource '{apiResource.Target.Name}' does not have a compute environment. " +
                "Configure an Azure deployment environment before adding it to API Management.");
        }
 
        if (computeEnvironment.TryGetLastAnnotation<InternalLoadBalancerAnnotation>(out var internalLoadBalancer))
        {
            var apiManagementNetwork = apiResource.Parent.VirtualNetworkConfiguration;
            if (apiManagementNetwork is null ||
                !ReferenceEquals(apiManagementNetwork.Subnet.Parent, internalLoadBalancer.VirtualNetwork))
            {
                throw new InvalidOperationException(
                    $"Resource '{apiResource.Target.Name}' is deployed to an internal Container Apps environment. " +
                    $"API Management resource '{apiResource.Parent.Name}' must be injected into the same virtual network.");
            }
        }
 
        var backendIdentifier = CreateGeneratedBicepIdentifier("computeBackend", apiIdentifier);
        var backendName = CreateBoundedIdentifier($"{apiIdentifier}Backend", 80);
        var endpointExpression = computeEnvironment.GetEndpointPropertyExpression(
            endpoint.Property(EndpointProperty.Url));
        var computeBackendUrl = endpointExpression.AsProvisioningParameter(
            infrastructure,
            CreateGeneratedBicepIdentifier("computeBackendUrl", apiIdentifier));
        var importedServiceUrl = new SafeMemberExpression(
            new MemberExpression(new IdentifierExpression(api.BicepIdentifier), "properties"),
            "serviceUrl");
        BicepValue<string> backendUrl = apiResource.OpenApiSource is AzureApiManagementOpenApiEndpoint
            // APIM resolves the document's servers entry during import. Reuse that value so a
            // non-root server path is not discarded when the policy selects the named backend.
            // Documents without a server URL can omit serviceUrl or leave it empty, so retain
            // the compute endpoint as a fallback.
            ? new ConditionalExpression(
                new FunctionCallExpression(new IdentifierExpression("empty"), importedServiceUrl),
                computeBackendUrl.Value.Compile(),
                importedServiceUrl)
            : computeBackendUrl;
 
        var backend = new ApiManagementBackend(backendIdentifier)
        {
            Parent = service,
            Name = backendName,
            Protocol = BackendProtocol.Http,
            Uri = new BicepValue<Uri>(backendUrl.Compile()),
            Title = apiResource.DisplayName,
            TypePropertiesType = BackendType.Single,
            Tls = new BackendTlsProperties
            {
                ShouldValidateCertificateChain = true,
                ShouldValidateCertificateName = true,
            },
        };
        infrastructure.Add(backend);
 
        return (backendName, backend, null);
    }
 
    private static ReferenceExpression ResolveOpenApiEndpoint(
        AzureApiManagementApiResource apiResource,
        AzureApiManagementOpenApiEndpoint source)
    {
        Debug.Assert(apiResource.Target is IResourceWithEndpoints);
        var endpointResource = (IResourceWithEndpoints)apiResource.Target;
        var endpoint = source.EndpointName is not null
            ? endpointResource.GetEndpoint(source.EndpointName)
            : endpointResource.GetEndpoints()
                .FirstOrDefault(candidate =>
                    candidate.EndpointAnnotation.IsExternal &&
                    candidate.EndpointAnnotation.UriScheme is "http" or "https");
 
        if (endpoint is null ||
            !endpoint.EndpointAnnotation.IsExternal ||
            endpoint.EndpointAnnotation.UriScheme is not ("http" or "https"))
        {
            throw new InvalidOperationException(
                $"Resource '{apiResource.Target.Name}' does not expose a suitable external HTTP or HTTPS endpoint for OpenAPI import. " +
                "Use WithOpenApiDocument for a private backend.");
        }
 
        if (!ComputeEnvironmentEndpointResolver.TryGetEffectiveComputeEnvironment(apiResource.Target, out var computeEnvironment))
        {
            throw new InvalidOperationException(
                $"Resource '{apiResource.Target.Name}' does not have a compute environment. " +
                "Configure an Azure deployment environment before importing its OpenAPI endpoint.");
        }
 
        if (computeEnvironment.HasAnnotationOfType<InternalLoadBalancerAnnotation>())
        {
            throw new InvalidOperationException(
                $"Resource '{apiResource.Target.Name}' is deployed to an internal Container Apps environment. " +
                "The API Management control plane cannot retrieve its OpenAPI document. Use WithOpenApiDocument instead.");
        }
 
        var endpointExpression = computeEnvironment.GetEndpointPropertyExpression(
            endpoint.Property(EndpointProperty.Url));
        return ReferenceExpression.Create($"{endpointExpression}{source.Path}");
    }
 
    private static Dictionary<AzureApiManagementBackendResource, ApiManagementBackend> AddBackends(
        AzureResourceInfrastructure infrastructure,
        AzureApiManagementResource azureResource,
        ApiManagementService service)
    {
        var provisionedBackends = new Dictionary<AzureApiManagementBackendResource, ApiManagementBackend>();
        foreach (var backendResource in azureResource.Backends)
        {
            var backendIdentifier = Infrastructure.NormalizeBicepIdentifier(backendResource.Name);
            var backend = new ApiManagementBackend(backendIdentifier)
            {
                Parent = service,
                Name = backendResource.BackendName,
                Protocol = GetProvisioningBackendProtocol(backendResource.Options.Protocol),
                Uri = new BicepValue<Uri>(backendResource.UriExpression.AsProvisioningParameter(
                    infrastructure,
                    CreateGeneratedBicepIdentifier("backendUrl", backendIdentifier)).Value.Compile()),
                Title = backendResource.Options.Title ?? backendResource.Name,
                TypePropertiesType = BackendType.Single,
                Tls = new BackendTlsProperties
                {
                    ShouldValidateCertificateChain = backendResource.Options.ValidateCertificateChain,
                    ShouldValidateCertificateName = backendResource.Options.ValidateCertificateName,
                },
            };
            if (backendResource.Options.CircuitBreaker is { } circuitBreaker)
            {
                backend.CircuitBreakerRules.Add(CreateCircuitBreakerRule(circuitBreaker));
            }
            infrastructure.Add(backend);
            provisionedBackends.Add(backendResource, backend);
        }
 
        return provisionedBackends;
    }
 
    private static void ValidateBackendPhysicalNames(AzureApiManagementResource azureResource)
    {
        var backendNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
 
        foreach (var backendName in azureResource.Backends.Select(backend => backend.BackendName)
                     .Concat(azureResource.BackendPools.Select(pool => pool.BackendPoolName)))
        {
            backendNames.Add(backendName);
        }
 
        foreach (var apiResource in azureResource.Apis.Where(api => api.Target is not null))
        {
            var apiIdentifier = Infrastructure.NormalizeBicepIdentifier(apiResource.Name);
            var backendName = CreateBoundedIdentifier($"{apiIdentifier}Backend", 80);
            if (!backendNames.Add(backendName))
            {
                throw new InvalidOperationException(
                    $"API Management API '{apiResource.Name}' generates backend physical name '{backendName}', " +
                    "which is already used by another backend or backend pool.");
            }
        }
    }
 
    private static Dictionary<AzureApiManagementBackendPoolResource, ApiManagementBackend> AddBackendPools(
        AzureResourceInfrastructure infrastructure,
        AzureApiManagementResource azureResource,
        ApiManagementService service,
        IReadOnlyDictionary<AzureApiManagementBackendResource, ApiManagementBackend> provisionedBackends)
    {
        var provisionedPools = new Dictionary<AzureApiManagementBackendPoolResource, ApiManagementBackend>();
        foreach (var poolResource in azureResource.BackendPools)
        {
            if (poolResource.Backends.Count == 0)
            {
                throw new InvalidOperationException(
                    $"API Management backend pool '{poolResource.Name}' does not contain any backends.");
            }
 
            var poolIdentifier = Infrastructure.NormalizeBicepIdentifier(poolResource.Name);
            var pool = new ApiManagementBackend(poolIdentifier)
            {
                Parent = service,
                Name = poolResource.BackendPoolName,
                Title = poolResource.DisplayName,
                TypePropertiesType = BackendType.Pool,
            };
 
            foreach (var member in poolResource.Backends)
            {
                var backend = provisionedBackends[member.Backend];
                pool.PoolServices.Add(new BackendPoolItem
                {
                    Id = backend.Id,
                    Priority = member.Priority,
                    Weight = member.Weight,
                });
                pool.DependsOn.Add(backend);
            }
 
            infrastructure.Add(pool);
            provisionedPools.Add(poolResource, pool);
        }
 
        return provisionedPools;
    }
 
    private static (string Identifier, ApiManagementBackend Backend, string? ManagedIdentityResource)
        ResolveApiManagementBackend(
            AzureApiManagementApiResource apiResource,
            IReadOnlyDictionary<AzureApiManagementBackendResource, ApiManagementBackend> provisionedBackends,
            IReadOnlyDictionary<AzureApiManagementBackendPoolResource, ApiManagementBackend> provisionedBackendPools)
    {
        return apiResource.Backend switch
        {
            AzureApiManagementBackendResource backend =>
                (backend.BackendName, provisionedBackends[backend], backend.Options.ManagedIdentityResource),
            AzureApiManagementBackendPoolResource pool =>
                (pool.BackendPoolName, provisionedBackendPools[pool], pool.Backends[0].Backend.Options.ManagedIdentityResource),
            null => throw new InvalidOperationException(
                $"API '{apiResource.Name}' does not have a backend. Call {nameof(WithBackend)} before deployment."),
            _ => throw new UnreachableException(),
        };
    }
 
    private static CircuitBreakerRule CreateCircuitBreakerRule(
        AzureApiManagementCircuitBreakerOptions options)
    {
        var rule = new CircuitBreakerRule
        {
            Name = options.Name,
            FailureCondition = new CircuitBreakerFailureCondition
            {
                Count = options.FailureCount,
                // Azure.Provisioning serializes TimeSpan literals as "00:00:30", but APIM requires ISO 8601.
                // https://learn.microsoft.com/azure/templates/microsoft.apimanagement/service/backends#circuitbreakerfailurecondition
                Interval = new StringLiteralExpression(XmlConvert.ToString(TimeSpan.FromSeconds(options.FailureIntervalSeconds))),
            },
            TripDuration = new StringLiteralExpression(XmlConvert.ToString(TimeSpan.FromSeconds(options.TripDurationSeconds))),
            AcceptRetryAfter = options.AcceptRetryAfter,
        };
 
        foreach (var range in options.StatusCodeRanges)
        {
            rule.FailureCondition.StatusCodeRanges.Add(new FailureStatusCodeRange
            {
                Min = range.Minimum,
                Max = range.Maximum,
            });
        }
 
        return rule;
    }
 
    private static ProvisionableResource AddOperation(
        AzureResourceInfrastructure infrastructure,
        AzureApiManagementOperationResource operationResource,
        ApiManagementApi api,
        IReadOnlyList<PolicyFragmentContract> policyFragments,
        IReadOnlyList<ApiManagementNamedValue> namedValues)
    {
        var operationIdentifier = Infrastructure.NormalizeBicepIdentifier(operationResource.Name);
        var operation = new ApiOperation(
            operationIdentifier)
        {
            Parent = api,
            Name = operationResource.OperationName,
            DisplayName = operationResource.DisplayName,
            Method = operationResource.Method,
            UriTemplate = operationResource.UrlTemplate,
        };
 
        foreach (var parameterName in GetTemplateParameterNames(operationResource.UrlTemplate))
        {
            operation.TemplateParameters.Add(new ParameterContract
            {
                Name = parameterName,
                ParameterContractType = "string",
                IsRequired = true,
            });
        }
 
        infrastructure.Add(operation);
 
        var policyXml = operationResource.PolicyXml ??
            CreatePolicyDocument(operationResource.InboundPolicyStatements);
 
        if (policyXml is null)
        {
            return operation;
        }
 
        var policy = new ApiOperationPolicy(
            CreateGeneratedBicepIdentifier("operationPolicy", operationIdentifier))
        {
            Parent = operation,
            Name = "policy",
            Format = PolicyContentFormat.RawXml,
            Value = policyXml,
        };
        foreach (var policyFragment in policyFragments)
        {
            policy.DependsOn.Add(policyFragment);
        }
        foreach (var namedValue in namedValues)
        {
            policy.DependsOn.Add(namedValue);
        }
        infrastructure.Add(policy);
 
        return policy;
    }
 
    private static IResourceBuilder<AzureApiManagementNamedValueResource> AddNamedValueCore(
        IResourceBuilder<AzureApiManagementResource> builder,
        string name,
        object value,
        bool secret,
        string? displayName,
        string? namedValueName,
        string[]? tags)
    {
        ArgumentNullException.ThrowIfNull(builder);
        ArgumentException.ThrowIfNullOrEmpty(name);
 
        var resolvedNamedValueName = namedValueName ?? name;
        var resolvedDisplayName = displayName ?? name;
        ValidateGeneralIdentifier(resolvedNamedValueName, 256, nameof(namedValueName));
        if (resolvedDisplayName.Length is 0 or > 256 ||
            resolvedDisplayName.Any(character => !char.IsLetterOrDigit(character) && character is not ('-' or '.' or '_')))
        {
            throw new ArgumentException(
                "The named-value display name must contain between 1 and 256 letters, digits, hyphens, periods, or underscores.",
                nameof(displayName));
        }
        if (builder.Resource.NamedValues.Any(namedValue =>
            string.Equals(namedValue.NamedValueName, resolvedNamedValueName, StringComparison.OrdinalIgnoreCase) ||
            string.Equals(namedValue.DisplayName, resolvedDisplayName, StringComparison.OrdinalIgnoreCase)))
        {
            throw new InvalidOperationException(
                $"An API Management named value with physical name '{resolvedNamedValueName}' or display name '{resolvedDisplayName}' has already been added.");
        }
 
        var resource = new AzureApiManagementNamedValueResource(
            name,
            resolvedNamedValueName,
            resolvedDisplayName,
            value,
            secret,
            tags ?? [],
            builder.Resource);
        builder.Resource.NamedValues.Add(resource);
 
        var resourceBuilder = builder.ApplicationBuilder.ExecutionContext.IsRunMode
            ? builder.ApplicationBuilder.CreateResourceBuilder(resource)
            : builder.ApplicationBuilder.AddResource(resource);
 
        return resourceBuilder
            .WithParentRelationship(builder.Resource)
            .ExcludeFromManifest()
            .WithIconName(secret ? "LockClosed" : "BracesVariable");
    }
 
    private static void ValidatePolicyFragmentParent(
        AzureApiManagementResource parent,
        IResourceBuilder<AzureApiManagementPolicyFragmentResource> fragment)
    {
        ArgumentNullException.ThrowIfNull(fragment);
        if (!ReferenceEquals(parent, fragment.Resource.Parent))
        {
            throw new InvalidOperationException(
                "An API Management policy can only include policy fragments from the same API Management service.");
        }
    }
 
    private static string CreateIncludeFragmentPolicy(string fragmentName) =>
        $"<include-fragment fragment-id=\"{fragmentName}\" />";
 
    private static string CreatePolicyFragmentDocument(string policyXml)
    {
        var builder = new StringBuilder();
        builder.Append("<fragment>\n");
        foreach (var line in policyXml.Split('\n'))
        {
            builder.Append("  ").Append(line.TrimEnd('\r')).Append('\n');
        }
        builder.Append("</fragment>");
        return builder.ToString();
    }
 
    private static HostnameType GetProvisioningHostnameType(AzureApiManagementHostnameType type) =>
        type switch
        {
            AzureApiManagementHostnameType.ConfigurationApi => HostnameType.ConfigurationApi,
            AzureApiManagementHostnameType.DeveloperPortal => HostnameType.DeveloperPortal,
            AzureApiManagementHostnameType.Portal => HostnameType.Portal,
            AzureApiManagementHostnameType.Proxy => HostnameType.Proxy,
            AzureApiManagementHostnameType.Management => HostnameType.Management,
            AzureApiManagementHostnameType.Scm => HostnameType.Scm,
            _ => throw new UnreachableException(),
        };
 
    private static TraceVerbosityLevel GetProvisioningVerbosity(AzureApiManagementDiagnosticVerbosity verbosity) =>
        verbosity switch
        {
            AzureApiManagementDiagnosticVerbosity.Error => TraceVerbosityLevel.Error,
            AzureApiManagementDiagnosticVerbosity.Information => TraceVerbosityLevel.Information,
            AzureApiManagementDiagnosticVerbosity.Verbose => TraceVerbosityLevel.Verbose,
            _ => throw new UnreachableException(),
        };
 
    private static BackendProtocol GetProvisioningBackendProtocol(AzureApiManagementBackendProtocol protocol) =>
        protocol switch
        {
            AzureApiManagementBackendProtocol.Http => BackendProtocol.Http,
            AzureApiManagementBackendProtocol.Soap => BackendProtocol.Soap,
            _ => throw new UnreachableException(),
        };
 
    private static void ValidateBackendOptions(
        AzureApiManagementResource service,
        AzureApiManagementBackendOptions options)
    {
        if (string.IsNullOrWhiteSpace(options.ManagedIdentityResource) &&
            options.ManagedIdentityResource is not null)
        {
            throw new ArgumentException(
                "The managed-identity resource URI cannot be empty.",
                nameof(options));
        }
 
        var circuitBreaker = options.CircuitBreaker;
        if (circuitBreaker is null)
        {
            return;
        }
 
        if (service.Options.Sku == AzureApiManagementSku.Consumption)
        {
            throw new InvalidOperationException(
                "API Management backend circuit breakers are not supported by the Consumption SKU.");
        }
        ArgumentException.ThrowIfNullOrEmpty(circuitBreaker.Name);
        ArgumentOutOfRangeException.ThrowIfLessThan(circuitBreaker.FailureCount, 1);
        ArgumentOutOfRangeException.ThrowIfLessThan(circuitBreaker.FailureIntervalSeconds, 1);
        ArgumentOutOfRangeException.ThrowIfLessThan(circuitBreaker.TripDurationSeconds, 1);
 
        foreach (var range in circuitBreaker.StatusCodeRanges)
        {
            if (range.Minimum is < 200 or > 599)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(options),
                    range.Minimum,
                    "The minimum circuit-breaker status code must be between 200 and 599.");
            }
            if (range.Maximum < range.Minimum || range.Maximum > 599)
            {
                throw new ArgumentOutOfRangeException(
                    nameof(options),
                    range.Maximum,
                    "The maximum circuit-breaker status code must be between the minimum status code and 599.");
            }
        }
        if (circuitBreaker.StatusCodeRanges.Length > 10)
        {
            throw new ArgumentException(
                "An API Management circuit-breaker rule cannot contain more than 10 status-code ranges.",
                nameof(options));
        }
    }
 
    private static void ValidateDisplayName(string value, string description, string parameterName)
    {
        if (string.IsNullOrWhiteSpace(value) || value.Length > 300)
        {
            throw new ArgumentException(
                $"The {description} must be between 1 and 300 characters.",
                parameterName);
        }
    }
 
    private static void ValidateMaximumLength(string value, int maximumLength, string description, string parameterName)
    {
        if (value.Length > maximumLength)
        {
            throw new ArgumentException($"{description} cannot exceed {maximumLength} characters.", parameterName);
        }
    }
 
    private static void ValidateDiagnosticOptions(AzureApiManagementDiagnosticOptions options)
    {
        if (!double.IsFinite(options.SamplingPercentage) ||
            options.SamplingPercentage is < 0 or > 100)
        {
            throw new ArgumentOutOfRangeException(
                nameof(options),
                options.SamplingPercentage,
                "The Application Insights sampling percentage must be between 0 and 100.");
        }
    }
 
    private static void ValidateGeneralIdentifier(string identifier, int maximumLength, string parameterName)
    {
        ArgumentException.ThrowIfNullOrEmpty(identifier, parameterName);
        if (identifier.Length > maximumLength)
        {
            throw new ArgumentException(
                $"The API Management identifier cannot exceed {maximumLength} characters.",
                parameterName);
        }
        if (identifier.IndexOfAny(s_invalidApiIdentifierCharacters) >= 0)
        {
            throw new ArgumentException(
                "The API Management identifier cannot contain '*', '#', '&', '+', ':', '<', '>', or '?'.",
                parameterName);
        }
    }
 
    private static void ValidatePolicyFragmentIdentifier(string identifier, string parameterName)
    {
        ArgumentException.ThrowIfNullOrEmpty(identifier, parameterName);
        if (identifier.Length > 80)
        {
            throw new ArgumentException("The policy-fragment identifier cannot exceed 80 characters.", parameterName);
        }
 
        static bool IsWordCharacter(char value) => char.IsLetterOrDigit(value) || value == '_';
 
        if (!IsWordCharacter(identifier[0]) ||
            !IsWordCharacter(identifier[^1]) ||
            identifier.Any(character => !IsWordCharacter(character) && character != '-'))
        {
            throw new ArgumentException(
                "The policy-fragment identifier may contain letters, digits, underscores, and non-leading or trailing hyphens.",
                parameterName);
        }
    }
 
    private static BicepValue<string>? CreatePolicyDocument(
        IReadOnlyList<string> inboundStatements,
        string? backendIdentifier = null,
        bool inheritParentPolicy = true,
        string? managedIdentityResource = null,
        BicepValue<string>? managedIdentityClientId = null)
    {
        if (backendIdentifier is null && inboundStatements.Count == 0)
        {
            return null;
        }
 
        var builder = new StringBuilder();
        builder.Append("<policies>\n");
        builder.Append("  <inbound>\n");
        if (inheritParentPolicy)
        {
            builder.Append("    <base />\n");
        }
 
        string? prefix = null;
        if (managedIdentityResource is not null)
        {
            Debug.Assert(managedIdentityClientId is not null);
            // APIM defaults to its system identity unless client-id selects the attached UAMI.
            // https://learn.microsoft.com/azure/api-management/authentication-managed-identity-policy
            builder.Append("    <authentication-managed-identity ")
                .Append(new XAttribute("resource", managedIdentityResource))
                .Append(" client-id=\"");
            prefix = builder.ToString();
            builder.Clear();
            builder.Append("\" />\n");
        }
 
        if (backendIdentifier is not null)
        {
            builder.Append("    <set-backend-service ")
                .Append(new XAttribute("backend-id", backendIdentifier))
                .Append(" />\n");
        }
 
        foreach (var statement in inboundStatements)
        {
            foreach (var line in statement.Split('\n'))
            {
                builder.Append("    ").Append(line.TrimEnd('\r')).Append('\n');
            }
        }
 
        builder.Append("  </inbound>\n");
        if (inheritParentPolicy)
        {
            builder.Append("  <backend><base /></backend>\n");
            builder.Append("  <outbound><base /></outbound>\n");
            builder.Append("  <on-error><base /></on-error>\n");
        }
        else
        {
            // A service policy is the root of the policy hierarchy, so <base /> has no parent
            // scope to inherit. It must forward the request explicitly instead.
            builder.Append("  <backend><forward-request /></backend>\n");
            builder.Append("  <outbound />\n");
            builder.Append("  <on-error />\n");
        }
        builder.Append("</policies>");
 
        return prefix is null
            ? new BicepValue<string>(builder.ToString())
            : BicepFunction.Interpolate($"{prefix}{managedIdentityClientId}{builder.ToString()}");
    }
 
    private static IEnumerable<string> GetTemplateParameterNames(string uriTemplate)
    {
        // APIM operation templates use placeholders such as:
        //   /products/{productId}/items/{itemId}?locale={locale}
        // Each distinct placeholder must have a matching templateParameters entry.
        var parameterNames = new HashSet<string>(StringComparer.Ordinal);
        var searchIndex = 0;
 
        while (searchIndex < uriTemplate.Length)
        {
            var openingBrace = uriTemplate.IndexOf('{', searchIndex);
            if (openingBrace < 0)
            {
                yield break;
            }
 
            var closingBrace = uriTemplate.IndexOf('}', openingBrace + 1);
            if (closingBrace < 0)
            {
                yield break;
            }
 
            var parameterName = uriTemplate[(openingBrace + 1)..closingBrace];
            if (parameterName.StartsWith('*'))
            {
                // Catch-all operation templates use /{*path}, while APIM expects the
                // corresponding templateParameters entry to be named "path".
                parameterName = parameterName[1..];
            }
 
            if (parameterName.Length > 0 && parameterNames.Add(parameterName))
            {
                yield return parameterName;
            }
 
            searchIndex = closingBrace + 1;
        }
    }
 
    private static string GetProxyOperationName(string method) =>
        $"{ProxyOperationName}-{method.ToLowerInvariant()}";
 
    private static string GetRootOperationName(string method) =>
        $"{GetProxyOperationName(method)}-root";
 
    private static string GetOperationSignature(string method, string urlTemplate)
    {
        // /products/{id}?expand={fields} and /products/{productId}?expand={selection}
        // describe the same route. Keep literal segments and wildcard markers, not parameter names.
        var template = new StringBuilder();
        for (var index = 0; index < urlTemplate.Length; index++)
        {
            if (urlTemplate[index] == '{' && urlTemplate.IndexOf('}', index + 1) is var end && end > index)
            {
                template.Append(urlTemplate[index + 1] == '*' ? "{*}" : "{}");
                index = end;
            }
            else
            {
                template.Append(urlTemplate[index]);
            }
        }
 
        var parts = template.ToString().Split('?', 2);
        var path = "/" + parts[0].TrimStart('/');
        var query = parts.Length == 2
            ? "?" + string.Join('&', parts[1].Split('&').Order(StringComparer.OrdinalIgnoreCase))
            : string.Empty;
        return $"{method.ToUpperInvariant()} {path}{query}";
    }
 
    private static void ValidatePolicyFragment(string policyXml)
    {
        ArgumentException.ThrowIfNullOrEmpty(policyXml);
 
        try
        {
            var document = XDocument.Parse($"<fragment>{policyXml}</fragment>");
            if (document.Root?.Elements().Any(e => e.Name.LocalName == "policies") is true)
            {
                throw new ArgumentException(
                    "An inbound policy statement must not contain a complete <policies> document.",
                    nameof(policyXml));
            }
        }
        catch (System.Xml.XmlException exception)
        {
            throw new ArgumentException("The policy statement must be well-formed XML.", nameof(policyXml), exception);
        }
    }
 
    private static void ValidatePolicyDocument(string policyXml)
    {
        ArgumentException.ThrowIfNullOrEmpty(policyXml);
 
        try
        {
            var document = XDocument.Parse(policyXml);
            if (document.Root?.Name.LocalName != "policies")
            {
                throw new ArgumentException(
                    "A complete policy document must have a <policies> root element.",
                    nameof(policyXml));
            }
 
        }
        catch (System.Xml.XmlException exception)
        {
            throw new ArgumentException("The policy document must be well-formed XML.", nameof(policyXml), exception);
        }
    }
 
    private static void EnsurePolicyFragmentCanBeAdded(string? completePolicyXml, string scope)
    {
        if (completePolicyXml is not null)
        {
            throw new InvalidOperationException(
                $"A complete {scope}-level policy has already been configured. " +
                $"{nameof(WithPolicy)} and {nameof(WithInboundPolicy)} cannot be combined at the same scope.");
        }
    }
 
    private static void EnsureCompletePolicyCanBeSet(IReadOnlyList<string> inboundPolicyStatements, string scope)
    {
        if (inboundPolicyStatements.Count > 0)
        {
            throw new InvalidOperationException(
                $"{scope}-level inbound policy statements have already been configured. " +
                $"{nameof(WithPolicy)} and {nameof(WithInboundPolicy)} cannot be combined at the same scope.");
        }
    }
 
    private static void ValidateApiIdentifier(string value, string parameterName)
    {
        ArgumentException.ThrowIfNullOrEmpty(value, parameterName);
 
        if (value.Length > 256)
        {
            throw new ArgumentException(
                "API Management API identifiers cannot exceed 256 characters.",
                parameterName);
        }
 
        if (value.IndexOfAny(s_invalidApiIdentifierCharacters) >= 0)
        {
            throw new ArgumentException(
                "API Management API identifiers cannot contain '*', '#', '&', '+', ':', '<', '>', or '?'.",
                parameterName);
        }
    }
 
    private static void ValidateOperationIdentifier(string value, string parameterName)
    {
        ArgumentException.ThrowIfNullOrEmpty(value, parameterName);
 
        if (value.Length > 80)
        {
            throw new ArgumentException(
                "API Management operation identifiers cannot exceed 80 characters.",
                parameterName);
        }
    }
 
    private static void ValidateApiUniqueness(
        AzureApiManagementResource service,
        string apiName,
        string normalizedPath)
    {
        if (service.Apis.Any(api => string.Equals(api.ApiName, apiName, StringComparison.OrdinalIgnoreCase)))
        {
            throw new InvalidOperationException(
                $"API Management service '{service.Name}' already contains an API with the physical identifier '{apiName}'.");
        }
 
        if (service.Apis.Any(api => string.Equals(api.Path, normalizedPath, StringComparison.OrdinalIgnoreCase)))
        {
            throw new InvalidOperationException(
                $"API Management service '{service.Name}' already contains an API at path '{normalizedPath}'.");
        }
    }
 
    internal static string CreateBoundedIdentifier(string value, int maximumLength)
    {
        if (value.Length <= maximumLength)
        {
            return value;
        }
 
        var hash = GetStableHash(value);
        return $"{value[..(maximumLength - hash.Length - 1)]}-{hash}";
    }
 
    private static string CreateGeneratedBicepIdentifier(string kind, params string[] resourceNames)
    {
        // Aspire resource names must start with a letter, so this prefix reserves a namespace for
        // generated APIM symbols that cannot collide with symbols derived from user resource names.
        var identifier = Infrastructure.NormalizeBicepIdentifier(
            string.Join('_', resourceNames.Prepend(kind).Prepend("apim").Prepend(string.Empty)));
 
        if (resourceNames.Length > 1)
        {
            // Joining normalized names is ambiguous when a separator can also occur within a name.
            // A null character cannot occur in an Aspire resource name, so it preserves component
            // boundaries when computing the stable identity suffix.
            return $"{identifier}_{GetStableHash(string.Join('\0', resourceNames))}";
        }
 
        return identifier;
    }
 
    private static string GetStableHash(string value) =>
        Convert.ToHexString(XxHash3.Hash(Encoding.UTF8.GetBytes(value))).ToLowerInvariant()[..8];
 
    private static AzureApiManagementOpenApiFormat InferOpenApiFormat(string path)
    {
        var pathWithoutQuery = path.Split('?', '#')[0];
        return Path.GetExtension(pathWithoutQuery).ToLowerInvariant() switch
        {
            ".json" => AzureApiManagementOpenApiFormat.OpenApiJson,
            ".yaml" or ".yml" => AzureApiManagementOpenApiFormat.OpenApi,
            _ => throw new ArgumentException(
                "The OpenAPI format cannot be inferred. Specify the format explicitly.",
                nameof(path)),
        };
    }
 
    private static ContentFormat GetOpenApiImportFormat(AzureApiManagementOpenApiFormat format, bool isLink) =>
        (format, isLink) switch
        {
            (AzureApiManagementOpenApiFormat.OpenApi, false) => ContentFormat.OpenApi,
            (AzureApiManagementOpenApiFormat.OpenApi, true) => ContentFormat.OpenApiLink,
            (AzureApiManagementOpenApiFormat.OpenApiJson, false) => ContentFormat.OpenApiJson,
            (AzureApiManagementOpenApiFormat.OpenApiJson, true) => ContentFormat.OpenApiJsonLink,
            (AzureApiManagementOpenApiFormat.SwaggerJson, false) => ContentFormat.SwaggerJson,
            (AzureApiManagementOpenApiFormat.SwaggerJson, true) => ContentFormat.SwaggerLinkJson,
            _ => throw new UnreachableException(),
        };
 
    // WithServiceDelegation is explicitly last-write-wins (see AzureVirtualNetworkExtensions), so a
    // subnet's delegation recorded at WithClassicVirtualNetwork/WithVirtualNetworkIntegration/
    // WithVirtualNetworkInjection call time can be replaced afterward by an unrelated later call on the
    // same subnet. Revalidate the subnet's *final* delegation here, during infrastructure generation,
    // so call order cannot produce an APIM service with an invalid or missing Azure delegation.
    private static void ValidateFinalSubnetDelegation(AzureApiManagementVirtualNetworkConfiguration network)
    {
        network.Subnet.TryGetLastAnnotation<AzureSubnetServiceDelegationAnnotation>(out var delegation);
        var requiredDelegation = network.Kind switch
        {
            AzureApiManagementVirtualNetworkKind.Classic => null,
            AzureApiManagementVirtualNetworkKind.V2Integration => AzureSubnetServiceDelegations.AppServiceEnvironments,
            AzureApiManagementVirtualNetworkKind.PremiumV2Injection => PremiumV2SubnetDelegation,
            _ => throw new UnreachableException(),
        };
 
        if (requiredDelegation is null)
        {
            if (delegation is not null)
            {
                throw new InvalidOperationException(
                    $"Subnet '{network.Subnet.Name}' is delegated to '{delegation.ServiceName}', " +
                    "but classic API Management virtual network injection requires an undelegated subnet.");
            }
 
            return;
        }
 
        if (delegation is null || !string.Equals(delegation.ServiceName, requiredDelegation, StringComparison.OrdinalIgnoreCase))
        {
            throw new InvalidOperationException(
                $"Subnet '{network.Subnet.Name}' must be delegated to '{requiredDelegation}' for API Management " +
                $"{(network.Kind == AzureApiManagementVirtualNetworkKind.PremiumV2Injection ? "virtual network injection" : "virtual network integration")}, " +
                $"but it is {(delegation is null ? "no longer delegated" : $"delegated to '{delegation.ServiceName}'")}.");
        }
    }
 
    private static void ConfigureV2SubnetDelegation(
        IResourceBuilder<AzureSubnetResource> subnet,
        string delegation)
    {
        if (subnet.Resource.TryGetLastAnnotation<AzureSubnetServiceDelegationAnnotation>(out var existingDelegation))
        {
            if (!string.Equals(existingDelegation.ServiceName, delegation, StringComparison.OrdinalIgnoreCase))
            {
                throw new InvalidOperationException(
                    $"Subnet '{subnet.Resource.Name}' is delegated to '{existingDelegation.ServiceName}', " +
                    $"but API Management requires delegation to '{delegation}'.");
            }
 
            return;
        }
 
        subnet.WithServiceDelegation(delegation);
    }
 
    private static void ValidateSubnetOwnership(
        AzureApiManagementResource apiManagement,
        AzureSubnetResource subnet)
    {
        var existingUsage = subnet.Annotations
            .OfType<AzureApiManagementSubnetUsageAnnotation>()
            .FirstOrDefault();
        if (existingUsage is not null && !ReferenceEquals(existingUsage.ApiManagement, apiManagement))
        {
            throw new InvalidOperationException(
                $"Subnet '{subnet.Name}' is already used by API Management resource '{existingUsage.ApiManagement.Name}'. " +
                "API Management networking requires a dedicated subnet for each service.");
        }
    }
 
    private static void ConfigureV2SubnetNetworkSecurityGroup(
        IResourceBuilder<AzureSubnetResource> subnet)
    {
        subnet.AllowOutbound(
            port: "443",
            from: AzureServiceTags.VirtualNetwork,
            to: AzureServiceTags.AzureKeyVault,
            protocol: SecurityRuleProtocol.Tcp,
            priority: GetKeyVaultRulePriority(subnet.Resource),
            name: "allow-apim-key-vault");
    }
 
    private static int GetKeyVaultRulePriority(AzureSubnetResource subnet)
    {
        // Both v2 networking modes require APIM's Azure Key Vault dependency. A late allow can be
        // shadowed by an outbound deny; conservatively place it ahead of all TCP/443 denies because
        // address prefixes may contain service tags or deploy-time expressions.
        // See https://learn.microsoft.com/azure/api-management/integrate-vnet-outbound#network-security-group.
        var rules = subnet.NetworkSecurityGroup?.SecurityRules;
        if (rules?.Any(rule => string.Equals(rule.Name, "allow-apim-key-vault", StringComparison.OrdinalIgnoreCase)) == true)
        {
            throw new InvalidOperationException(
                $"Subnet '{subnet.Name}' already contains the reserved NSG rule 'allow-apim-key-vault'.");
        }
        var firstDenyPriority = rules?
            .Where(CouldDenyKeyVault)
            .Select(rule => rule.Priority)
            .DefaultIfEmpty(4097)
            .Min() ?? 4097;
        var priority = Enumerable.Range(100, Math.Clamp(firstDenyPriority - 100, 0, 3997))
            .FirstOrDefault(candidate => rules?.Any(rule => rule.Priority == candidate) != true);
        if (priority == 0)
        {
            throw new InvalidOperationException(
                $"Subnet '{subnet.Name}' has no free NSG priority before its outbound deny rules. " +
                "Reserve an earlier priority for the API Management Key Vault allow rule.");
        }
 
        return priority;
    }
 
    private static bool CouldDenyKeyVault(AzureSecurityRule rule)
    {
        if (rule.Direction != SecurityRuleDirection.Outbound ||
            rule.Access != SecurityRuleAccess.Deny ||
            (rule.Protocol != SecurityRuleProtocol.Tcp && rule.Protocol != SecurityRuleProtocol.Asterisk))
        {
            return false;
        }
 
        if (rule.DestinationPortRange is "*" or "443")
        {
            return true;
        }
 
        // NSG port ranges use "start-end", such as "400-500".
        var ports = rule.DestinationPortRange.Split('-', 2);
        return ports.Length == 2 &&
            int.TryParse(ports[0], out var first) &&
            int.TryParse(ports[1], out var last) &&
            first <= 443 && last >= 443;
    }
 
    private static void ValidateV2SubnetSize(AzureSubnetResource subnet)
    {
        if (subnet.AddressPrefix is not { } addressPrefix ||
            !int.TryParse(addressPrefix[(addressPrefix.LastIndexOf('/') + 1)..], out var prefixLength))
        {
            return;
        }
 
        if (prefixLength > 27)
        {
            throw new InvalidOperationException(
                $"Subnet '{subnet.Name}' uses '{addressPrefix}', but API Management v2 networking requires a /27 or larger subnet.");
        }
    }
 
    private static bool SetVirtualNetworkConfiguration(
        AzureApiManagementResource resource,
        AzureSubnetResource subnet,
        AzureApiManagementVirtualNetworkMode mode,
        AzureApiManagementVirtualNetworkKind kind)
    {
        if (resource.VirtualNetworkConfiguration is { } existing &&
            (!ReferenceEquals(existing.Subnet, subnet) ||
             existing.Mode != mode ||
             existing.Kind != kind))
        {
            throw new InvalidOperationException(
                $"API Management resource '{resource.Name}' already has a different virtual network configuration.");
        }
 
        if (resource.VirtualNetworkConfiguration is not null)
        {
            return false;
        }
 
        resource.VirtualNetworkConfiguration = new(subnet, mode, kind);
        return true;
    }
 
    private static void ValidateCapacity(AzureApiManagementSku sku, int capacity)
    {
        var (minimum, maximum) = sku switch
        {
            AzureApiManagementSku.Consumption => (0, 0),
            AzureApiManagementSku.Developer => (1, 1),
            AzureApiManagementSku.Basic => (1, 2),
            AzureApiManagementSku.BasicV2 => (1, 10),
            AzureApiManagementSku.Standard => (1, 4),
            AzureApiManagementSku.StandardV2 => (1, 10),
            AzureApiManagementSku.Premium => (1, 12),
            AzureApiManagementSku.PremiumV2 => (1, 30),
            _ => throw new UnreachableException(),
        };
 
        if (capacity < minimum || capacity > maximum)
        {
            throw new ArgumentException(
                $"Capacity for SKU '{sku}' must be between {minimum} and {maximum}.",
                nameof(capacity));
        }
    }
 
    private static BicepValue<ApiManagementServiceSkuType> GetProvisioningSku(AzureApiManagementSku sku) =>
        sku switch
        {
            AzureApiManagementSku.Consumption => ApiManagementServiceSkuType.Consumption,
            AzureApiManagementSku.Developer => ApiManagementServiceSkuType.Developer,
            AzureApiManagementSku.Basic => ApiManagementServiceSkuType.Basic,
            AzureApiManagementSku.BasicV2 => ApiManagementServiceSkuType.BasicV2,
            AzureApiManagementSku.Standard => ApiManagementServiceSkuType.Standard,
            AzureApiManagementSku.StandardV2 => ApiManagementServiceSkuType.StandardV2,
            AzureApiManagementSku.Premium => ApiManagementServiceSkuType.Premium,
            // The published SDK enum predates PremiumV2; emit the ARM value without duplicating its SKU model.
            // https://learn.microsoft.com/azure/templates/microsoft.apimanagement/2025-03-01-preview/service#apimanagementserviceskuproperties
            AzureApiManagementSku.PremiumV2 => new StringLiteralExpression("PremiumV2"),
            _ => throw new UnreachableException(),
        };
}