// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#pragma warning disable ASPIREPIPELINES001
#pragma warning disable ASPIREAZURE001
#pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Azure;
using Aspire.Hosting.Azure.AppContainers;
using Aspire.Hosting.Pipelines;
using Azure.Provisioning;
using Azure.Provisioning.AppContainers;
using Azure.Provisioning.ContainerRegistry;
using Azure.Provisioning.Expressions;
using Azure.Provisioning.OperationalInsights;
using Azure.Provisioning.Primitives;
using Azure.Provisioning.Roles;
using Azure.Provisioning.Storage;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using FileShare = Azure.Provisioning.Storage.FileShare;
namespace Aspire.Hosting;
/// <summary>
/// Provides extension methods for customizing Azure Container App definitions for projects.
/// </summary>
public static class AzureContainerAppExtensions
{
internal const string PrepareContainerAppsStepNamePrefix = "prepare-azure-container-apps-";
internal const string ValidateContainerAppsStepName = "validate-azure-container-apps";
/// <summary>
/// Adds the necessary infrastructure for Azure Container Apps to the distributed application builder.
/// </summary>
/// <param name="builder">The distributed application builder.</param>
[Obsolete($"Use {nameof(AddAzureContainerAppEnvironment)} instead. This method will be removed in a future version.")]
public static IDistributedApplicationBuilder AddAzureContainerAppsInfrastructure(this IDistributedApplicationBuilder builder) =>
AddAzureContainerAppsInfrastructureCore(builder);
internal static IDistributedApplicationBuilder AddAzureContainerAppsInfrastructureCore(this IDistributedApplicationBuilder builder)
{
ArgumentNullException.ThrowIfNull(builder);
builder.AddAzureProvisioning();
// The per-environment prepare-azure-container-apps-{name} steps handle role assignments,
// so Azure resources don't need to add the default role assignments themselves
builder.Services.Configure<AzureProvisioningOptions>(o => o.SupportsTargetedRoleAssignments = true);
// Register the pipeline step idempotently. AddAzureContainerAppsInfrastructureCore can be
// called more than once (e.g. when AddAzureContainerAppEnvironment is called for multiple
// environments). The marker singleton ensures we only add the step the first time.
//
// The per-environment work (creating ContainerApp resources and DeploymentTargetAnnotations)
// is registered as a separate per-environment pipeline step on AzureContainerAppEnvironmentResource.
// This global step only validates that no resource has a PublishAs* annotation when there are
// no AzureContainerAppEnvironmentResource instances in the model.
if (builder.Services.All(d => d.ServiceType != typeof(ContainerAppsPipelineStepMarker)))
{
var marker = new ContainerAppsPipelineStepMarker();
builder.Services.AddSingleton(marker);
builder.Pipeline.AddStep(
name: ValidateContainerAppsStepName,
action: ctx =>
{
if (!ctx.ExecutionContext.IsPublishMode)
{
return Task.CompletedTask;
}
var environments = ctx.Model.Resources
.OfType<AzureContainerAppEnvironmentResource>()
.Where(environment => !environment.IsExcludedFromPublish())
.ToList();
if (environments.Count == 0)
{
foreach (var r in ctx.Model.GetComputeResources())
{
if (r.HasAnnotationOfType<AzureContainerAppCustomizationAnnotation>() ||
r.HasAnnotationOfType<AzureContainerAppJobCustomizationAnnotation>())
{
throw new InvalidOperationException($"Resource '{r.Name}' is configured to publish as an Azure Container App, but there are no '{nameof(AzureContainerAppEnvironmentResource)}' resources. Ensure you have added one by calling '{nameof(AddAzureContainerAppEnvironment)}'.");
}
}
}
else
{
// Name resolvers run while each environment's Bicep module is generated. Force evaluation
// here so the shared tracker sees every legacy fallback before deployment targets are prepared.
foreach (var environment in environments)
{
_ = environment.GetBicepTemplateString();
}
marker.ValidateManagedEnvironmentNames(
environments.Select(environment => environment.Name).ToHashSet(StringComparer.Ordinal));
}
return Task.CompletedTask;
},
dependsOn: AzureEnvironmentResource.PrepareResourcesStepName,
requiredBy: WellKnownPipelineSteps.BeforeStart);
builder.Pipeline.AddPipelineConfiguration(context =>
{
var validationStep = context.Steps.Single(step => step.Name == ValidateContainerAppsStepName);
var configuredBuildOptions = context.Services
.GetRequiredService<IOptions<AzureProvisioningOptions>>()
.Value
.ProvisioningBuildOptions;
foreach (var environment in context.Model.Resources.OfType<AzureContainerAppEnvironmentResource>())
{
// Pipeline-level configuration runs before AzureBicepResource forces template generation.
// Assign configured resolvers now so the collision tracker records the final fallback name,
// rather than a stale legacy expression generated before azure-prepare-resources executes.
environment.ProvisioningBuildOptions ??= configuredBuildOptions;
var prepareStep = context.GetSteps(environment)
.SingleOrDefault(step => step.Name == $"{PrepareContainerAppsStepNamePrefix}{environment.Name}");
prepareStep?.DependsOn(validationStep);
}
return Task.CompletedTask;
});
}
return builder;
}
private sealed class ContainerAppsPipelineStepMarker
{
private const string ResourceGroupTokenPlaceholder = "\u001f\u001f\u001f\u001f\u001f\u001f\u001f\u001f\u001f\u001f\u001f\u001f\u001f";
private readonly object _lock = new();
private readonly Dictionary<string, Dictionary<string, ManagedEnvironmentName>> _environmentsByManagedEnvironmentName = new(StringComparer.Ordinal);
public void RecordManagedEnvironmentName(
string environmentName,
BicepValue<string> managedEnvironmentName,
ManagedEnvironmentNamingMode namingMode)
{
var expression = managedEnvironmentName.ToString();
var effectiveNameKey = GetEffectiveNameKey(expression);
lock (_lock)
{
if (!_environmentsByManagedEnvironmentName.TryGetValue(effectiveNameKey, out var environmentNames))
{
environmentNames = new Dictionary<string, ManagedEnvironmentName>(StringComparer.Ordinal);
_environmentsByManagedEnvironmentName.Add(effectiveNameKey, environmentNames);
}
environmentNames[environmentName] = new ManagedEnvironmentName(expression, namingMode);
}
}
public void ValidateManagedEnvironmentNames(IReadOnlySet<string> includedEnvironmentNames)
{
KeyValuePair<string, ManagedEnvironmentName>[][] collisions;
lock (_lock)
{
collisions = _environmentsByManagedEnvironmentName
.Select(pair => pair.Value.Where(environment => includedEnvironmentNames.Contains(environment.Key)).ToArray())
.Where(environments => environments.Length > 1)
.ToArray();
}
if (collisions.Length == 0)
{
return;
}
var collisionDetails = string.Join(
"; ",
collisions.SelectMany(environments =>
environments
.GroupBy(environment => environment.Value.Expression, StringComparer.Ordinal)
.Select(group =>
$"'{string.Join("', '", group.Select(environment => environment.Key).Order(StringComparer.Ordinal))}' resolve to {group.Key}")));
var namingModes = collisions
.SelectMany(environments => environments)
.Select(environment => environment.Value.NamingMode)
.ToHashSet();
var guidance = new List<string>();
if (namingModes.Contains(ManagedEnvironmentNamingMode.Legacy))
{
guidance.Add($"For environments using the default naming convention, call '{nameof(WithUniqueResourceNaming)}()'.");
}
if (namingModes.Contains(ManagedEnvironmentNamingMode.Unique))
{
guidance.Add($"For environments already using '{nameof(WithUniqueResourceNaming)}()', rename one or more resources or configure an explicit name resolver.");
}
if (namingModes.Contains(ManagedEnvironmentNamingMode.Azd))
{
guidance.Add($"For environments using '{nameof(WithAzdResourceNaming)}()', remove it or configure distinct managed environment names explicitly.");
}
throw new DistributedApplicationException(
$"Azure Container App environments {collisionDetails}. Multiple environments with the same managed environment name cannot be deployed to one resource group. " +
string.Join(" ", guidance));
}
private static string GetEffectiveNameKey(string expression)
{
// Normalize the two generated Bicep shapes that use the same 13-character resource-group token:
// take('cae-${uniqueString(resourceGroup().id)}', 60)
// 'cae-${resourceToken}'
// Evaluating take after substitution catches syntactically different expressions that deploy the
// same physical name. Unknown shapes retain their expression as the key.
var normalizedExpression = expression
.Replace("${uniqueString(resourceGroup().id)}", ResourceGroupTokenPlaceholder, StringComparison.Ordinal)
.Replace("${resourceToken}", ResourceGroupTokenPlaceholder, StringComparison.Ordinal);
const string TakePrefix = "take('";
const string TakeSeparator = "', ";
if (normalizedExpression.StartsWith(TakePrefix, StringComparison.Ordinal) &&
normalizedExpression.EndsWith(')'))
{
var separatorIndex = normalizedExpression.LastIndexOf(TakeSeparator, StringComparison.Ordinal);
var lengthStart = separatorIndex + TakeSeparator.Length;
if (separatorIndex >= TakePrefix.Length &&
int.TryParse(
normalizedExpression.AsSpan(lengthStart, normalizedExpression.Length - lengthStart - 1),
out var maxLength))
{
var value = normalizedExpression[TakePrefix.Length..separatorIndex];
return value[..Math.Min(value.Length, maxLength)];
}
}
if (normalizedExpression is ['\'', .., '\''])
{
return normalizedExpression[1..^1];
}
return normalizedExpression;
}
}
private enum ManagedEnvironmentNamingMode
{
Legacy,
Unique,
Azd
}
private readonly record struct ManagedEnvironmentName(string Expression, ManagedEnvironmentNamingMode NamingMode);
/// <summary>
/// Adds a container app environment resource to the distributed application builder.
/// </summary>
/// <param name="builder">The distributed application builder.</param>
/// <param name="name">The name of the resource.</param>
/// <returns><see cref="IResourceBuilder{T}"/></returns>
/// <ats-returns>The resource builder.</ats-returns>
[AspireExport]
public static IResourceBuilder<AzureContainerAppEnvironmentResource> AddAzureContainerAppEnvironment(this IDistributedApplicationBuilder builder, string name)
{
builder.AddAzureContainerAppsInfrastructureCore();
var marker = builder.Services
.Where(descriptor => descriptor.ServiceType == typeof(ContainerAppsPipelineStepMarker))
.Select(descriptor => descriptor.ImplementationInstance)
.OfType<ContainerAppsPipelineStepMarker>()
.Single();
var containerAppEnvResource = new AzureContainerAppEnvironmentResource(name, infra =>
{
var appEnvResource = (AzureContainerAppEnvironmentResource)infra.AspireResource;
// When the user has marked this environment as existing (via AsExisting / PublishAsExisting),
// we must not generate a brand-new managed environment + Log Analytics + Dashboard. Instead,
// emit a thin module that references the existing environment and still wires up the ACR pull
// identity that newly-deployed container apps in the env will need.
// See https://github.com/microsoft/aspire/issues/12977.
if (appEnvResource.IsExisting())
{
ConfigureExistingContainerAppEnvironmentInfrastructure(infra, appEnvResource);
return;
}
// This tells azd to avoid creating infrastructure
var userPrincipalId = new ProvisioningParameter(AzureBicepResource.KnownParameters.UserPrincipalId, typeof(string)) { Value = new BicepValue<string>(string.Empty) };
infra.Add(userPrincipalId);
var tags = new ProvisioningParameter("tags", typeof(object))
{
Value = new BicepDictionary<string>()
};
infra.Add(tags);
ProvisioningVariable? resourceToken = null;
if (appEnvResource.UseAzdNamingConvention || appEnvResource.UseCompactResourceNaming)
{
resourceToken = new ProvisioningVariable("resourceToken", typeof(string))
{
Value = BicepFunction.GetUniqueString(BicepFunction.GetResourceGroup().Id)
};
infra.Add(resourceToken);
}
UserAssignedIdentity? newIdentity = null;
BicepValue<string> managedIdentityIdOutputValue;
if (appEnvResource.TryGetLastAnnotation<AzureContainerAppEnvironmentAcrPullIdentityAnnotation>(out var identityAnnotation))
{
// The user has supplied an existing identity (commonly via AddAzureUserAssignedIdentity +
// .WithRoleAssignments(acr, AcrPull)). Skip creating env_mi + the AcrPull role assignment
// here and have the env module read the identity id from a parameter wired to the identity
// module's "id" output.
managedIdentityIdOutputValue = identityAnnotation.Identity.Id.AsProvisioningParameter(infra);
}
else
{
newIdentity = new UserAssignedIdentity(Infrastructure.NormalizeBicepIdentifier($"{appEnvResource.Name}_mi"))
{
Tags = tags
};
infra.Add(newIdentity);
managedIdentityIdOutputValue = newIdentity.Id.ToBicepExpression();
}
AzureProvisioningResource? registry = null;
if (appEnvResource.TryGetLastAnnotation<ContainerRegistryReferenceAnnotation>(out var registryReferenceAnnotation) &&
registryReferenceAnnotation.Registry is AzureProvisioningResource explicitRegistry)
{
registry = explicitRegistry;
}
else if (appEnvResource.DefaultContainerRegistry is not null)
{
registry = appEnvResource.DefaultContainerRegistry;
}
if (registry is null)
{
throw new InvalidOperationException($"No container registry associated with environment '{appEnvResource.Name}'. This should have been added automatically.");
}
var containerRegistry = (ContainerRegistryService)registry.AddAsExistingResource(infra);
infra.Add(containerRegistry);
if (newIdentity is not null)
{
var pullRa = containerRegistry.CreateRoleAssignment(ContainerRegistryBuiltInRole.AcrPull, newIdentity);
// There's a bug in the CDK, see https://github.com/Azure/azure-sdk-for-net/issues/47265
pullRa.Name = BicepFunction.CreateGuid(containerRegistry.Id, newIdentity.Id, pullRa.RoleDefinitionId);
infra.Add(pullRa);
}
OperationalInsightsWorkspace? laWorkspace = null;
if (appEnvResource.TryGetLastAnnotation<AzureLogAnalyticsWorkspaceReferenceAnnotation>(out var logAnalyticsReferenceAnnotation) && logAnalyticsReferenceAnnotation.Workspace is AzureProvisioningResource workspace)
{
laWorkspace = (OperationalInsightsWorkspace)workspace.AddAsExistingResource(infra);
}
else
{
laWorkspace = new OperationalInsightsWorkspace(Infrastructure.NormalizeBicepIdentifier($"{appEnvResource.Name}_law"))
{
Sku = new() { Name = OperationalInsightsWorkspaceSkuName.PerGB2018 },
Tags = tags
};
}
infra.Add(laWorkspace);
var containerAppEnvironment = new ContainerAppManagedEnvironment(appEnvResource.GetBicepIdentifier())
{
WorkloadProfiles = [
new ContainerAppWorkloadProfile()
{
WorkloadProfileType = "Consumption",
Name = "consumption"
}
],
AppLogsConfiguration = new()
{
Destination = "log-analytics",
LogAnalyticsConfiguration = new()
{
CustomerId = laWorkspace.CustomerId,
SharedKey = laWorkspace.GetKeys().PrimarySharedKey
}
},
Tags = tags
};
// Configure VNet integration if a subnet is specified
if (appEnvResource.TryGetLastAnnotation<DelegatedSubnetAnnotation>(out var subnetAnnotation))
{
containerAppEnvironment.VnetConfiguration = new ContainerAppVnetConfiguration
{
InfrastructureSubnetId = subnetAnnotation.SubnetId.AsProvisioningParameter(infra)
};
}
infra.Add(containerAppEnvironment);
if (appEnvResource.EnableDashboard)
{
var dashboard = new ContainerAppEnvironmentDotnetComponentResource("aspireDashboard", "2025-10-02-preview")
{
Name = "aspire-dashboard",
ComponentType = "AspireDashboard",
Parent = containerAppEnvironment
};
infra.Add(dashboard);
}
var managedStorages = new Dictionary<string, ContainerAppManagedEnvironmentStorage>();
var resource = (AzureContainerAppEnvironmentResource)infra.AspireResource;
StorageAccount? storageVolume = null;
if (resource.VolumeNames.Count > 0)
{
storageVolume = new StorageAccount(Infrastructure.NormalizeBicepIdentifier($"{appEnvResource.Name}_storageVolume"))
{
Tags = tags,
Sku = new StorageSku() { Name = StorageSkuName.StandardLrs },
Kind = StorageKind.StorageV2,
LargeFileSharesState = LargeFileSharesState.Enabled,
MinimumTlsVersion = StorageMinimumTlsVersion.Tls1_2,
};
infra.Add(storageVolume);
var storageVolumeFileService = new FileService("storageVolumeFileService")
{
Parent = storageVolume
};
infra.Add(storageVolumeFileService);
foreach (var (outputName, output) in resource.VolumeNames)
{
var shareName = Infrastructure.NormalizeBicepIdentifier($"shares_{outputName}");
var managedStorageName = Infrastructure.NormalizeBicepIdentifier($"managedStorage_{outputName}");
var share = new FileShare(shareName)
{
Parent = storageVolumeFileService,
ShareQuota = 1024,
EnabledProtocol = FileShareEnabledProtocol.Smb
};
infra.Add(share);
var keysExpr = storageVolume.GetKeys()[0].Compile();
var keyValue = new MemberExpression(keysExpr, "value");
var containerAppStorage = new ContainerAppManagedEnvironmentStorage(managedStorageName)
{
Parent = containerAppEnvironment,
ManagedEnvironmentStorageAzureFile = new()
{
ShareName = share.Name,
AccountName = storageVolume.Name,
AccountKey = keyValue,
AccessMode = ContainerAppAccessMode.ReadWrite
}
};
infra.Add(containerAppStorage);
managedStorages[outputName] = containerAppStorage;
if (appEnvResource.UseAzdNamingConvention)
{
var volumeName = output.volume.Type switch
{
ContainerMountType.BindMount => $"bm{output.index}",
ContainerMountType.Volume => output.volume.Source ?? $"v{output.index}",
_ => throw new NotSupportedException()
};
// Remove '.' and '-' characters from volumeName
volumeName = volumeName.Replace(".", "").Replace("-", "");
share.Name = BicepFunction.Take(
BicepFunction.Interpolate(
$"{BicepFunction.ToLower(output.resource.Name)}-{BicepFunction.ToLower(volumeName)}"),
60);
containerAppStorage.Name = BicepFunction.Take(
BicepFunction.Interpolate(
$"{BicepFunction.ToLower(output.resource.Name)}-{BicepFunction.ToLower(volumeName)}"),
32);
}
else if (appEnvResource.UseCompactResourceNaming)
{
Debug.Assert(resourceToken is not null);
var volumeName = output.volume.Type switch
{
ContainerMountType.BindMount => $"bm{output.index}",
ContainerMountType.Volume => output.volume.Source ?? $"v{output.index}",
_ => throw new NotSupportedException()
};
// Remove '.' and '-' characters from volumeName
volumeName = volumeName.Replace(".", "").Replace("-", "");
share.Name = BicepFunction.Take(
BicepFunction.Interpolate(
$"{BicepFunction.ToLower(output.resource.Name)}-{BicepFunction.ToLower(volumeName)}"),
60);
containerAppStorage.Name = BicepFunction.Take(
BicepFunction.Interpolate(
$"{BicepFunction.ToLower(output.resource.Name)}-{BicepFunction.ToLower(volumeName)}-{resourceToken}"),
32);
}
}
}
// Add the volume outputs to the container app environment storage
foreach (var (key, value) in managedStorages)
{
infra.Add(new ProvisioningOutput(key, typeof(string))
{
// use an expression here in case the resource's Name was set to a function expression above
Value = new MemberExpression(new IdentifierExpression(value.BicepIdentifier), "name")
});
}
if (appEnvResource.UseAzdNamingConvention)
{
Debug.Assert(resourceToken is not null);
newIdentity?.Name = BicepFunction.Interpolate($"mi-{resourceToken}");
containerRegistry.Name = new FunctionCallExpression(
new IdentifierExpression("replace"),
new InterpolatedStringExpression([
new StringLiteralExpression("acr-"),
new IdentifierExpression(resourceToken.BicepIdentifier)
]),
new StringLiteralExpression("-"),
new StringLiteralExpression(""));
laWorkspace.Name = BicepFunction.Interpolate($"law-{resourceToken}");
var managedEnvironmentName = BicepFunction.Interpolate($"cae-{resourceToken}");
containerAppEnvironment.Name = managedEnvironmentName;
if (!appEnvResource.IsExcludedFromPublish())
{
marker.RecordManagedEnvironmentName(
appEnvResource.Name,
managedEnvironmentName,
ManagedEnvironmentNamingMode.Azd);
}
#pragma warning disable IDE0031 // Use null propagation (IDE0031)
if (storageVolume is not null)
#pragma warning restore IDE0031
{
storageVolume.Name = BicepFunction.Interpolate($"vol{resourceToken}");
}
}
else if (appEnvResource.UseCompactResourceNaming)
{
Debug.Assert(resourceToken is not null);
if (storageVolume is not null)
{
// Sanitize env name for storage accounts: lowercase alphanumeric only.
// Reserve 2 chars for "sv" prefix + 13 for uniqueString = 15, leaving 9 for the env name.
var sanitizedPrefix = new string(appEnvResource.Name.ToLowerInvariant()
.Where(c => char.IsLetterOrDigit(c)).ToArray());
if (sanitizedPrefix.Length > 9)
{
sanitizedPrefix = sanitizedPrefix[..9];
}
storageVolume.Name = BicepFunction.Take(
BicepFunction.Interpolate($"{sanitizedPrefix}sv{resourceToken}"),
24);
}
}
// By default the managed environment name is left to Azure.Provisioning, whose sanitizer keeps
// only lowercase letters (ContainerAppManagedEnvironment inherits ResourceNameRequirements(1, 24,
// ResourceNameCharacters.LowercaseLetters)). That drops digits from the bicep identifier, so
// environments named e.g. "cae1"/"cae2" in the same resource group both resolve to
// take('cae${uniqueString(resourceGroup().id)}', 24) and collapse onto a single physical
// environment, where concurrent container-app writes race with ManagedEnvironmentOperationInProgress
// (see https://github.com/microsoft/aspire/issues/18722).
//
// Changing this by default would rename already-deployed environments and cause Azure to recreate
// them, so the digit-preserving name is opt-in via WithUniqueResourceNaming(). The fallback resolver
// also records the actual generated expression so publish can reject colliding legacy names with
// actionable guidance instead of allowing a broken deployment.
if (!appEnvResource.UseAzdNamingConvention && !appEnvResource.IsExcludedFromPublish())
{
AddManagedEnvironmentNameResolver(appEnvResource, containerAppEnvironment, marker);
}
// Exposed so that callers reference the LA workspace in other bicep modules
infra.Add(new ProvisioningOutput("AZURE_LOG_ANALYTICS_WORKSPACE_NAME", typeof(string))
{
Value = laWorkspace.Name.ToBicepExpression()
});
infra.Add(new ProvisioningOutput("AZURE_LOG_ANALYTICS_WORKSPACE_ID", typeof(string))
{
Value = laWorkspace.Id.ToBicepExpression()
});
AddSharedContainerAppEnvironmentOutputs(infra, containerRegistry, containerAppEnvironment, managedIdentityIdOutputValue);
});
// Create the default container registry resource before creating the environment
var registryName = $"{name}-acr";
var defaultRegistry = CreateDefaultAzureContainerRegistry(builder, registryName, containerAppEnvResource);
containerAppEnvResource.DefaultContainerRegistry = defaultRegistry;
// Create the resource builder first, then attach the registry to avoid recreating builders
var appEnvBuilder = builder.ExecutionContext.IsRunMode
// HACK: We need to return a valid resource builder for the container app environment
// but in run mode, we don't want to add the resource to the builder.
? builder.CreateResourceBuilder(containerAppEnvResource)
: builder.AddResource(containerAppEnvResource);
// The environment normally creates its ACR pull identity and role assignment inline. When the final
// registry is explicitly cross-scoped, late preparation promotes only that identity to a standalone
// resource so the role assignment can be emitted in a module scoped to the registry.
appEnvBuilder.WithCrossScopeAcrPullIdentity(
identity => new AzureContainerAppEnvironmentAcrPullIdentityAnnotation(identity),
identityBuilder =>
{
// Standalone identities use compact naming by default, but azd-named environments have
// historically used mi-${resourceToken}. Preserve that physical name for this promoted path.
if (!containerAppEnvResource.UseAzdNamingConvention)
{
return;
}
identityBuilder.ConfigureInfrastructure(infrastructure =>
{
var resourceToken = new ProvisioningVariable("resourceToken", typeof(string))
{
Value = BicepFunction.GetUniqueString(BicepFunction.GetResourceGroup().Id)
};
infrastructure.Add(resourceToken);
var identity = infrastructure.GetProvisionableResources().OfType<UserAssignedIdentity>().Single();
identity.Name = BicepFunction.Interpolate($"mi-{resourceToken}");
});
});
return appEnvBuilder;
}
/// <summary>
/// Adds the fallback resolver used to track generated managed environment names when azd naming is not in
/// effect, substituting the corrected name requirements when <see cref="WithUniqueResourceNaming"/> is applied.
/// </summary>
/// <remarks>
/// Azure.Provisioning's default name resolver only assigns names while the <c>Name</c> property is unset.
/// Registering a resolver rather than assigning <c>Name</c> here preserves any caller-supplied resolvers in
/// <see cref="ProvisioningBuildOptions.InfrastructureResolvers"/> while still fixing the default fallback.
/// </remarks>
private static void AddManagedEnvironmentNameResolver(
AzureContainerAppEnvironmentResource appEnvResource,
ContainerAppManagedEnvironment managedEnvironment,
ContainerAppsPipelineStepMarker marker)
{
var options = appEnvResource.ProvisioningBuildOptions ?? new ProvisioningBuildOptions();
if (options.InfrastructureResolvers.OfType<ManagedEnvironmentNameResolver>()
.Any(resolver => ReferenceEquals(resolver.ManagedEnvironment, managedEnvironment)))
{
return;
}
// AzureResourcePreparer assigns the same ProvisioningBuildOptions instance to every provisioning resource.
// Deployment builds independent resource templates concurrently, so mutating that shared resolver list here
// would race with other builds that are enumerating it. Copy the caller-configured options first, then add
// the managed-environment fallback only to this resource's build options.
options = CloneProvisioningBuildOptions(options);
appEnvResource.ProvisioningBuildOptions = options;
var resolver = new ManagedEnvironmentNameResolver(
managedEnvironment,
appEnvResource.Name,
appEnvResource.UseUniqueResourceNaming,
marker);
// Put Aspire's resolver after caller-configured resolvers but before Azure.Provisioning's default dynamic
// resolver. This preserves explicit policies such as AspireV8ResourceNamePropertyResolver while preventing
// the built-in lowercase-letters-only fallback from collapsing "cae1" and "cae2" to the same name.
var defaultDynamicResolverIndex = -1;
for (var i = 0; i < options.InfrastructureResolvers.Count; i++)
{
if (options.InfrastructureResolvers[i].GetType() == typeof(DynamicResourceNamePropertyResolver))
{
defaultDynamicResolverIndex = i;
break;
}
}
if (defaultDynamicResolverIndex >= 0)
{
options.InfrastructureResolvers.Insert(defaultDynamicResolverIndex, resolver);
}
else
{
options.InfrastructureResolvers.Add(resolver);
}
}
private static ProvisioningBuildOptions CloneProvisioningBuildOptions(ProvisioningBuildOptions options)
{
var clone = new ProvisioningBuildOptions
{
Random = options.Random
};
clone.InfrastructureResolvers.Clear();
foreach (var infrastructureResolver in options.InfrastructureResolvers)
{
clone.InfrastructureResolvers.Add(infrastructureResolver);
}
return clone;
}
/// <summary>
/// Configures the Bicep infrastructure for an <see cref="AzureContainerAppEnvironmentResource"/> that has been
/// marked as existing (e.g. via <c>AsExisting</c> / <c>PublishAsExisting</c>). Only emits a reference to the
/// existing managed environment and the small set of supporting resources that newly-deployed container apps
/// need (an ACR-pull managed identity bound to the env's container registry).
/// </summary>
/// <remarks>
/// We deliberately skip:
/// <list type="bullet">
/// <item><description>Creating a new <c>ContainerAppManagedEnvironment</c> — the existing one is referenced.</description></item>
/// <item><description>Creating a new <c>OperationalInsightsWorkspace</c> — the existing env already has logging configured.</description></item>
/// <item><description>Creating the Aspire Dashboard <c>dotNetComponent</c> — the existing env already owns its dashboard configuration.</description></item>
/// <item><description>Storage accounts / file shares for container volumes and VNet configuration — these cannot be
/// added to an env that already exists without mutating it. If the user attempts to combine these features with
/// <c>AsExisting</c>, we throw a clear error.</description></item>
/// </list>
/// See https://github.com/microsoft/aspire/issues/12977 for the bug this addresses.
/// </remarks>
private static void ConfigureExistingContainerAppEnvironmentInfrastructure(
AzureResourceInfrastructure infra,
AzureContainerAppEnvironmentResource appEnvResource)
{
if (appEnvResource.VolumeNames.Count > 0)
{
throw new InvalidOperationException(
$"The Azure Container App Environment '{appEnvResource.Name}' is marked as existing but one or more " +
"container apps targeted to it declare volume mounts. Volumes require provisioning storage on the " +
"managed environment, which Aspire cannot do for an existing environment. Remove the volume mounts " +
"or stop marking the environment as existing.");
}
if (appEnvResource.HasAnnotationOfType<DelegatedSubnetAnnotation>())
{
throw new InvalidOperationException(
$"The Azure Container App Environment '{appEnvResource.Name}' is marked as existing but is also " +
"configured with a delegated subnet via WithDelegatedSubnet. VNet integration is a property of the " +
"managed environment itself and cannot be reconfigured on an existing environment. Remove the " +
"WithDelegatedSubnet call or stop marking the environment as existing.");
}
if (appEnvResource.HasAnnotationOfType<AzureLogAnalyticsWorkspaceReferenceAnnotation>())
{
throw new InvalidOperationException(
$"The Azure Container App Environment '{appEnvResource.Name}' is marked as existing but is also " +
"configured with a Log Analytics workspace via WithAzureLogAnalyticsWorkspace. The existing managed " +
"environment already owns its Log Analytics workspace and Aspire cannot reconfigure it. Remove the " +
"WithAzureLogAnalyticsWorkspace call or stop marking the environment as existing.");
}
// This tells azd to avoid creating infrastructure.
var userPrincipalId = new ProvisioningParameter(AzureBicepResource.KnownParameters.UserPrincipalId, typeof(string)) { Value = new BicepValue<string>(string.Empty) };
infra.Add(userPrincipalId);
var tags = new ProvisioningParameter("tags", typeof(object))
{
Value = new BicepDictionary<string>()
};
infra.Add(tags);
// Reference the existing managed environment. AddAsExistingResource handles the
// FromExisting + ExistingAzureResourceAnnotation (name / resource group scope) wiring.
var containerAppEnvironment = (ContainerAppManagedEnvironment)appEnvResource.AddAsExistingResource(infra);
// Container apps still need an identity that can pull from the configured ACR. By default we
// create one here and add an AcrPull role assignment on the registry. When the user has supplied
// their own identity via WithAcrPullIdentity, we skip both — they own role assignments —
// and emit the supplied identity's id as AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID.
UserAssignedIdentity? newIdentity = null;
BicepValue<string> managedIdentityIdOutputValue;
if (appEnvResource.TryGetLastAnnotation<AzureContainerAppEnvironmentAcrPullIdentityAnnotation>(out var identityAnnotation))
{
managedIdentityIdOutputValue = identityAnnotation.Identity.Id.AsProvisioningParameter(infra);
}
else
{
newIdentity = new UserAssignedIdentity(Infrastructure.NormalizeBicepIdentifier($"{appEnvResource.Name}_mi"))
{
Tags = tags
};
infra.Add(newIdentity);
managedIdentityIdOutputValue = newIdentity.Id.ToBicepExpression();
}
AzureProvisioningResource? registry = null;
if (appEnvResource.TryGetLastAnnotation<ContainerRegistryReferenceAnnotation>(out var registryReferenceAnnotation) &&
registryReferenceAnnotation.Registry is AzureProvisioningResource explicitRegistry)
{
registry = explicitRegistry;
}
else if (appEnvResource.DefaultContainerRegistry is not null)
{
registry = appEnvResource.DefaultContainerRegistry;
}
if (registry is null)
{
throw new InvalidOperationException($"No container registry associated with environment '{appEnvResource.Name}'. This should have been added automatically.");
}
var containerRegistry = (ContainerRegistryService)registry.AddAsExistingResource(infra);
infra.Add(containerRegistry);
if (newIdentity is not null)
{
var pullRa = containerRegistry.CreateRoleAssignment(ContainerRegistryBuiltInRole.AcrPull, newIdentity);
// There's a bug in the CDK, see https://github.com/Azure/azure-sdk-for-net/issues/47265
pullRa.Name = BicepFunction.CreateGuid(containerRegistry.Id, newIdentity.Id, pullRa.RoleDefinitionId);
infra.Add(pullRa);
}
AddSharedContainerAppEnvironmentOutputs(infra, containerRegistry, containerAppEnvironment, managedIdentityIdOutputValue);
}
/// <summary>
/// Emits the container-registry + container-app-environment outputs that downstream
/// resources (<c>BaseContainerAppContext</c>, <c>ContainerAppUrls</c>, etc.) consume.
/// Shared between the greenfield and existing-env infrastructure callbacks so both
/// paths produce the exact same set of well-known outputs.
/// </summary>
private static void AddSharedContainerAppEnvironmentOutputs(
AzureResourceInfrastructure infra,
ContainerRegistryService containerRegistry,
ContainerAppManagedEnvironment containerAppEnvironment,
BicepValue<string> managedIdentityIdOutputValue)
{
// Required by the IContainerRegistry interface
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_NAME", typeof(string))
{
Value = containerRegistry.Name.ToBicepExpression()
});
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_ENDPOINT", typeof(string))
{
Value = containerRegistry.LoginServer.ToBicepExpression()
});
// Required by the IAzureContainerRegistry interface
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_REGISTRY_MANAGED_IDENTITY_ID", typeof(string))
{
Value = managedIdentityIdOutputValue
});
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_APPS_ENVIRONMENT_NAME", typeof(string))
{
Value = containerAppEnvironment.Name.ToBicepExpression()
});
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_APPS_ENVIRONMENT_ID", typeof(string))
{
Value = containerAppEnvironment.Id.ToBicepExpression()
});
// Required for azd to output the dashboard URL
infra.Add(new ProvisioningOutput("AZURE_CONTAINER_APPS_ENVIRONMENT_DEFAULT_DOMAIN", typeof(string))
{
Value = containerAppEnvironment.DefaultDomain.ToBicepExpression()
});
}
/// <summary>
/// Configures the container app environment resources to use the same naming conventions as azd.
/// </summary>
/// <param name="builder">The AzureContainerAppEnvironmentResource to configure.</param>
/// <returns><see cref="IResourceBuilder{T}"/></returns>
/// <ats-returns>The resource builder.</ats-returns>
/// <remarks>
/// By default, the container app environment resources use a different naming convention than azd.
///
/// This method allows for reusing the previously deployed resources if the application was deployed using
/// azd without calling <see cref="AddAzureContainerAppEnvironment"/>
/// </remarks>
[AspireExport]
public static IResourceBuilder<AzureContainerAppEnvironmentResource> WithAzdResourceNaming(this IResourceBuilder<AzureContainerAppEnvironmentResource> builder)
{
builder.Resource.UseAzdNamingConvention = true;
return builder;
}
/// <summary>
/// Configures the container app environment to use compact resource naming that maximally preserves
/// the <c>uniqueString</c> suffix for length-constrained Azure resources such as storage accounts.
/// </summary>
/// <param name="builder">The <see cref="AzureContainerAppEnvironmentResource"/> to configure.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for chaining.</returns>
/// <ats-returns>The resource builder.</ats-returns>
/// <remarks>
/// <para>
/// By default, the generated Azure resource names use long static suffixes (e.g. <c>storageVolume</c>,
/// <c>managedStorage</c>) that can consume most of the 24-character storage account name limit, truncating
/// the <c>uniqueString(resourceGroup().id)</c> portion that provides cross-deployment uniqueness.
/// </para>
/// <para>
/// When enabled, this method shortens the static portions of generated names so the full 13-character
/// <c>uniqueString</c> is preserved. This prevents naming collisions when deploying multiple environments
/// to different resource groups.
/// </para>
/// <para>
/// This option only affects volume-related storage resources. It does not change the naming of the
/// container app environment, container registry, log analytics workspace, or managed identity.
/// Use <see cref="WithAzdResourceNaming"/> to change those names as well.
/// </para>
/// </remarks>
[AspireExport]
[Experimental("ASPIREACANAMING001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public static IResourceBuilder<AzureContainerAppEnvironmentResource> WithCompactResourceNaming(this IResourceBuilder<AzureContainerAppEnvironmentResource> builder)
{
builder.Resource.UseCompactResourceNaming = true;
return builder;
}
/// <summary>
/// Configures the container app environment to incorporate its resource name into the generated managed
/// environment <c>name</c>, so that multiple environments in the same resource group get distinct names.
/// </summary>
/// <param name="builder">The <see cref="AzureContainerAppEnvironmentResource"/> to configure.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{T}"/> for chaining.</returns>
/// <ats-returns>The resource builder.</ats-returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> is null.</exception>
/// <remarks>
/// <para>
/// By default the managed environment relies on Azure.Provisioning's name, whose sanitizer keeps only
/// lowercase letters. For example, two <see cref="AddAzureContainerAppEnvironment"/> resources named
/// <c>cae1</c> and <c>cae2</c> both drop their trailing digit and yield
/// <c>take('cae${uniqueString(resourceGroup().id)}', 24)</c>. When they share a resource group,
/// that default would collapse both onto a single physical environment and concurrent
/// container-app writes would race with <c>ManagedEnvironmentOperationInProgress</c>. Publish and deploy
/// detect this collision and fail with guidance to apply this method instead
/// (see <see href="https://github.com/microsoft/aspire/issues/18722"/>).
/// </para>
/// <para>
/// Applying this method computes the managed environment <c>name</c> with the same algorithm Azure.Provisioning
/// uses for every other Azure resource type (sanitized resource name, then a hyphen separator and a
/// <c>uniqueString(resourceGroup().id)</c> suffix, truncated to the maximum length), but with the character
/// requirements Azure Container Apps managed environments actually allow — lowercase letters, digits, and
/// hyphens, with a 60-character maximum. For example, <c>cae1</c> produces
/// <c>take('cae1-${uniqueString(resourceGroup().id)}', 60)</c>, while <c>prod1</c> uses a <c>prod1</c>
/// prefix. Preserving the digits gives each environment a distinct <c>name</c>. This is opt-in because
/// enabling it changes the environment <c>name</c> for an already-deployed single environment, which would
/// cause Azure to recreate it. Apply it only to the environments that need distinct names (typically when
/// deploying more than one environment to a single resource group), or to new deployments.
/// </para>
/// <para>
/// This option acts as a fallback after name resolvers configured through
/// <see cref="AzureProvisioningOptions.ProvisioningBuildOptions"/>. A configured resolver can therefore
/// override the generated name. This option also has no effect when <see cref="WithAzdResourceNaming"/> is
/// used, since azd naming sets the environment name explicitly.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// var env1 = builder.AddAzureContainerAppEnvironment("cae1")
/// .WithUniqueResourceNaming();
/// var env2 = builder.AddAzureContainerAppEnvironment("cae2")
/// .WithUniqueResourceNaming();
/// </code>
/// </example>
[AspireExport]
[Experimental("ASPIREACANAMING002", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")]
public static IResourceBuilder<AzureContainerAppEnvironmentResource> WithUniqueResourceNaming(this IResourceBuilder<AzureContainerAppEnvironmentResource> builder)
{
ArgumentNullException.ThrowIfNull(builder);
builder.Resource.UseUniqueResourceNaming = true;
return builder;
}
/// <summary>
/// Configures whether the Aspire dashboard should be included in the container app environment.
/// </summary>
/// <param name="builder">The AzureContainerAppEnvironmentResource to configure.</param>
/// <param name="enable">Whether to include the Aspire dashboard. Default is true.</param>
/// <returns><see cref="IResourceBuilder{T}"/></returns>
/// <ats-returns>The resource builder.</ats-returns>
[AspireExport]
public static IResourceBuilder<AzureContainerAppEnvironmentResource> WithDashboard(this IResourceBuilder<AzureContainerAppEnvironmentResource> builder, bool enable = true)
{
builder.Resource.EnableDashboard = enable;
return builder;
}
/// <summary>
/// Configures whether HTTP endpoints should be upgraded to HTTPS in Azure Container Apps.
/// By default, HTTP endpoints are upgraded to HTTPS for security and WebSocket compatibility.
/// </summary>
/// <param name="builder">The AzureContainerAppEnvironmentResource to configure.</param>
/// <param name="upgrade">Whether to upgrade HTTP endpoints to HTTPS. Default is true.</param>
/// <returns><see cref="IResourceBuilder{T}"/></returns>
/// <ats-returns>The resource builder.</ats-returns>
/// <remarks>
/// When disabled (<c>false</c>), HTTP endpoints will use HTTP scheme and port 80 in Azure Container Apps.
/// Note that explicit ports specified for development (e.g., port 8080) are still normalized
/// to standard ports (80/443) as required by Azure Container Apps.
/// </remarks>
[AspireExport]
public static IResourceBuilder<AzureContainerAppEnvironmentResource> WithHttpsUpgrade(this IResourceBuilder<AzureContainerAppEnvironmentResource> builder, bool upgrade = true)
{
builder.Resource.PreserveHttpEndpoints = !upgrade;
return builder;
}
/// <summary>
/// Configures the container app environment resource to use the specified Log Analytics Workspace.
/// </summary>
/// <param name="builder">The AzureContainerAppEnvironmentResource to configure.</param>
/// <param name="workspaceBuilder">The resource builder for the <see cref="AzureLogAnalyticsWorkspaceResource"/> to use.</param>
/// <returns><see cref="IResourceBuilder{T}"/></returns>
/// <ats-returns>The resource builder.</ats-returns>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="workspaceBuilder"/> is null.</exception>
[AspireExport]
public static IResourceBuilder<AzureContainerAppEnvironmentResource> WithAzureLogAnalyticsWorkspace(this IResourceBuilder<AzureContainerAppEnvironmentResource> builder, IResourceBuilder<AzureLogAnalyticsWorkspaceResource> workspaceBuilder)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(workspaceBuilder);
// Add a LogAnalyticsWorkspaceReferenceAnnotation to indicate that the resource is using a specific workspace
builder.WithAnnotation(new AzureLogAnalyticsWorkspaceReferenceAnnotation(workspaceBuilder.Resource));
return builder;
}
/// <summary>
/// Configures the container app environment to use the supplied <see cref="AzureUserAssignedIdentityResource"/>
/// as the managed identity that container apps in the environment use to pull images from the configured
/// container registry (the <c>AcrPull</c> identity), instead of having Aspire create a new identity and a new
/// <c>AcrPull</c> role assignment.
/// </summary>
/// <param name="builder">The container app environment to configure.</param>
/// <param name="identityBuilder">
/// The resource builder for the user-assigned identity that should be used for image pulls. This identity is
/// only used for the <c>AcrPull</c> role; it is not assigned to individual container apps in the environment.
/// </param>
/// <returns>The <see cref="IResourceBuilder{T}"/> for chaining.</returns>
/// <remarks>
/// <para>
/// When this is set, Aspire will not create a new identity or an <c>AcrPull</c> role assignment for the
/// container registry. The caller is responsible for ensuring the supplied identity already has the required
/// <c>AcrPull</c> role assignment on the registry, for example by chaining
/// <c>.WithRoleAssignments(acr, ContainerRegistryBuiltInRole.AcrPull)</c> when adding the identity.
/// </para>
/// <para>
/// This is commonly combined with <c>AsExisting</c> on the environment and on the container registry to deploy
/// container apps into a pre-provisioned set of Azure resources without Aspire emitting any new identity or
/// role-assignment resources. See <see href="https://github.com/microsoft/aspire/issues/12977"/> for the
/// scenario this addresses.
/// </para>
/// <para>
/// Only the combination of an existing environment, an existing container registry, and this method avoids
/// emitting any new identity or role-assignment resources in the env module. Other combinations still work but
/// will emit additional resources:
/// </para>
/// <list type="bullet">
/// <item><description>
/// If the environment is marked <c>AsExisting</c> but no identity is supplied here, Aspire still emits a new
/// <c>UserAssignedIdentity</c> and an <c>AcrPull</c> role assignment on the configured registry.
/// </description></item>
/// <item><description>
/// If this method is used but the container registry is not existing (either the Aspire-generated default
/// registry or a user-added registry without <c>AsExisting</c>), the registry itself is still provisioned.
/// To wire the supplied identity to that newly-created registry, chain
/// <c>.WithRoleAssignments(acr, ContainerRegistryBuiltInRole.AcrPull)</c> on the identity.
/// </description></item>
/// </list>
/// </remarks>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="builder"/> or <paramref name="identityBuilder"/> is <see langword="null"/>.</exception>
[AspireExport]
public static IResourceBuilder<AzureContainerAppEnvironmentResource> WithAcrPullIdentity(
this IResourceBuilder<AzureContainerAppEnvironmentResource> builder,
IResourceBuilder<AzureUserAssignedIdentityResource> identityBuilder)
{
ArgumentNullException.ThrowIfNull(builder);
ArgumentNullException.ThrowIfNull(identityBuilder);
builder.WithAnnotation(new AzureContainerAppEnvironmentAcrPullIdentityAnnotation(identityBuilder.Resource));
return builder;
}
private sealed class ManagedEnvironmentNameResolver(
ContainerAppManagedEnvironment managedEnvironment,
string environmentName,
bool useUniqueResourceNaming,
ContainerAppsPipelineStepMarker marker) : DynamicResourceNamePropertyResolver
{
// Azure Container Apps managed environment names allow lowercase letters, digits, and hyphens and are
// 2-60 characters long. These are the managed-environment limits, not the 2-32 character container-app
// limits:
// https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.ContainerApp.EnvNaming/
//
// Azure.Provisioning.AppContainers does not override GetResourceNameRequirements for
// ContainerAppManagedEnvironment, so it inherits ProvisionableResource's conservative default of
// (1, 24, LowercaseLetters). That default silently drops the digits Aspire relies on to keep sibling
// environment names distinct, so "cae1"/"cae2" both sanitize to "cae" and collide in a shared resource
// group (https://github.com/microsoft/aspire/issues/18722).
private static readonly ResourceNameRequirements s_requirements = new(
minLength: 2,
maxLength: 60,
validCharacters: ResourceNameCharacters.LowercaseLetters | ResourceNameCharacters.Numbers | ResourceNameCharacters.Hyphen);
public ContainerAppManagedEnvironment ManagedEnvironment { get; } = managedEnvironment;
public override BicepValue<string>? ResolveName(ProvisioningBuildOptions options, ProvisionableResource resource, ResourceNameRequirements requirements)
{
// Scope strictly to the environment instance this resolver was created for, by reference identity.
// Bicep identifiers are only unique within a single module, so matching on the identifier could rename
// an unrelated ContainerAppManagedEnvironment that happens to share the same name in another module.
// Every other resource returns null so it falls through to the rest of the resolver chain unchanged.
if (!ReferenceEquals(resource, ManagedEnvironment))
{
return null;
}
// Delegate to the standard dynamic naming algorithm. The opt-in substitutes the requirements that
// Azure.Provisioning failed to declare; otherwise the supplied requirements preserve the legacy output
// byte-for-byte. Recording the returned expression here means caller-configured resolvers still take
// precedence and only names produced by this fallback participate in collision validation.
var resolvedName = base.ResolveName(
options,
resource,
useUniqueResourceNaming ? s_requirements : requirements);
if (resolvedName is not null)
{
marker.RecordManagedEnvironmentName(
environmentName,
resolvedName,
useUniqueResourceNaming ? ManagedEnvironmentNamingMode.Unique : ManagedEnvironmentNamingMode.Legacy);
}
return resolvedName;
}
}
private static AzureContainerRegistryResource CreateDefaultAzureContainerRegistry(IDistributedApplicationBuilder builder, string name, AzureContainerAppEnvironmentResource containerAppEnvironment)
{
var configureInfrastructure = (AzureResourceInfrastructure infrastructure) =>
{
ContainerRegistryInfrastructure.ConfigureContainerRegistry(infrastructure,
configureNewRegistry: (newRegistry, infra) =>
{
if (containerAppEnvironment.UseAzdNamingConvention)
{
var resourceToken = new ProvisioningVariable("resourceToken", typeof(string))
{
Value = BicepFunction.GetUniqueString(BicepFunction.GetResourceGroup().Id)
};
infra.Add(resourceToken);
newRegistry.Name = new FunctionCallExpression(
new IdentifierExpression("replace"),
new InterpolatedStringExpression([
new StringLiteralExpression("acr-"),
new IdentifierExpression(resourceToken.BicepIdentifier)
]),
new StringLiteralExpression("-"),
new StringLiteralExpression(""));
}
});
};
var resource = new AzureContainerRegistryResource(name, configureInfrastructure);
if (builder.ExecutionContext.IsPublishMode)
{
builder.AddResource(resource).WithIconName("Archive");
}
return resource;
}
}