diff --git a/src/Aspire.Hosting.Azure/AzurePublisher.cs b/src/Aspire.Hosting.Azure/AzurePublisher.cs
index 4ff11d035fe..0c3c9044c00 100644
--- a/src/Aspire.Hosting.Azure/AzurePublisher.cs
+++ b/src/Aspire.Hosting.Azure/AzurePublisher.cs
@@ -4,16 +4,28 @@
using System.Diagnostics.CodeAnalysis;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Publishing;
-using Azure.Provisioning;
-using Azure.Provisioning.Expressions;
-using Azure.Provisioning.Primitives;
-using Azure.Provisioning.Resources;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Aspire.Hosting.Azure;
+///
+/// Represents a publisher for deploying distributed application models to Azure using Bicep templates.
+///
+///
+/// This class is responsible for processing a distributed application model, generating Bicep templates,
+/// and configuring Azure infrastructure for deployment. It supports parameter resolution, resource grouping,
+/// and output propagation for Azure resources.
+///
+///
+/// Example usage:
+///
+/// var publisher = new AzurePublisher("myPublisher", optionsMonitor, provisioningOptions, logger);
+/// await publisher.PublishAsync(model, cancellationToken);
+///
+///
+///
[Experimental("ASPIREAZURE001", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")]
internal sealed class AzurePublisher(
[ServiceKey] string name,
@@ -21,272 +33,21 @@ internal sealed class AzurePublisher(
IOptions provisioningOptions,
ILogger logger) : IDistributedApplicationPublisher
{
- private AzureProvisioningOptions ProvisioningOptions => provisioningOptions.Value;
-
- public Task PublishAsync(DistributedApplicationModel model, CancellationToken cancellationToken)
+ ///
+ /// Publishes the specified distributed application model to Azure using Bicep templates.
+ ///
+ /// The distributed application model to publish.
+ /// A token to monitor for cancellation requests.
+ /// A task that represents the asynchronous publish operation.
+ public async Task PublishAsync(DistributedApplicationModel model, CancellationToken cancellationToken)
{
var publisherOptions = options.Get(name);
- var infra = new Infrastructure
- {
- TargetScope = DeploymentScope.Subscription
- };
-
- var environmentParam = new ProvisioningParameter("environmentName", typeof(string));
- infra.Add(environmentParam);
-
- var locationParam = new ProvisioningParameter("location", typeof(string));
- infra.Add(locationParam);
-
- var principalId = new ProvisioningParameter("principalId", typeof(string));
- infra.Add(principalId);
-
- var tags = new ProvisioningVariable("tags", typeof(object))
- {
- Value = new BicepDictionary
- {
- ["aspire-env-name"] = environmentParam
- }
- };
-
- // REVIEW: Do we want people to be able to change this
- var rg = new ResourceGroup("rg")
- {
- Name = BicepFunction.Interpolate($"rg-{environmentParam}"),
- Location = locationParam,
- Tags = tags
- };
-
var outputDirectory = new DirectoryInfo(publisherOptions.OutputPath!);
-
outputDirectory.Create();
- // Process the resources in the model and create a module for each one
- var moduleMap = new Dictionary();
-
- foreach (var resource in model.Resources.OfType())
- {
- var file = resource.GetBicepTemplateFile();
-
- var moduleDirectory = outputDirectory.CreateSubdirectory(resource.Name);
-
- var modulePath = Path.Combine(moduleDirectory.FullName, $"{resource.Name}.bicep");
-
- File.Copy(file.Path, modulePath, true);
-
- var identifier = Infrastructure.NormalizeBicepIdentifier(resource.Name);
-
- var module = new ModuleImport(identifier, $"{resource.Name}/{resource.Name}.bicep")
- {
- Name = resource.Name
- };
-
- moduleMap[resource] = module;
- }
-
- // Resolve parameters *after* writing the modules to disk
- // this is because some parameters are added in ConfigureInfrastructure callbacks
- var parameterMap = new Dictionary();
-
- foreach (var resource in model.Resources.OfType())
- {
- foreach (var parameter in resource.Parameters)
- {
- Visit(parameter.Value, v =>
- {
- if (v is ParameterResource p && !parameterMap.ContainsKey(p))
- {
- var pid = Infrastructure.NormalizeBicepIdentifier(p.Name);
-
- var pp = new ProvisioningParameter(pid, typeof(string))
- {
- IsSecure = p.Secret
- };
-
- if (!p.Secret && p.Default is not null)
- {
- pp.Value = p.Value;
- }
-
- // Map the parameter to the Bicep parameter
- parameterMap[p] = pp;
- }
- });
- }
- }
-
- static BicepValue GetOutputs(ModuleImport module, string outputName) =>
- new MemberExpression(new MemberExpression(new IdentifierExpression(module.BicepIdentifier), "outputs"), outputName);
-
- BicepFormatString EvalExpr(ReferenceExpression expr)
- {
- var args = new object[expr.ValueProviders.Count];
-
- for (var i = 0; i < expr.ValueProviders.Count; i++)
- {
- args[i] = Eval(expr.ValueProviders[i]);
- }
-
- return new BicepFormatString(expr.Format, args);
- }
-
- object Eval(object? value) => value switch
- {
- BicepOutputReference b => GetOutputs(moduleMap[b.Resource], b.Name),
- ParameterResource p => parameterMap[p],
- ConnectionStringReference r => Eval(r.Resource.ConnectionStringExpression),
- IResourceWithConnectionString cs => Eval(cs.ConnectionStringExpression),
- ReferenceExpression re => EvalExpr(re),
- string s => s,
- _ => ""
- };
-
- static BicepValue ResolveValue(object val)
- {
- return val switch
- {
- BicepValue s => s,
- string s => s,
- ProvisioningParameter p => p,
- BicepFormatString fs => BicepFunction2.Interpolate(fs),
- _ => throw new NotSupportedException("Unsupported value type " + val.GetType())
- };
- }
-
- foreach (var resource in model.Resources.OfType())
- {
- BicepValue scope = resource.Scope?.ResourceGroup switch
- {
- // resourceGroup(rgName)
- string rgName => new FunctionCallExpression(new IdentifierExpression("resourceGroup"), new StringLiteralExpression(rgName)),
- ParameterResource p => parameterMap[p],
- _ => new IdentifierExpression(rg.BicepIdentifier)
- };
-
- var module = moduleMap[resource];
- module.Scope = scope;
- module.Parameters.Add("location", locationParam);
-
- foreach (var parameter in resource.Parameters)
- {
- // TODO: There are a set of known parameter names that we may not be able to resolve.
- // This is from earlier versions of aspire where infra was split across
- // azd and aspire. Once the infra moves to aspire, we can throw for
- // unresolved "known parameters".
-
- if (parameter.Key == AzureBicepResource.KnownParameters.UserPrincipalId && parameter.Value is null)
- {
- module.Parameters.Add(parameter.Key, principalId);
- continue;
- }
-
- var value = ResolveValue(Eval(parameter.Value));
-
- module.Parameters.Add(parameter.Key, value);
- }
- }
-
- var outputs = new Dictionary();
-
- // Now find all resources that have deployment targets that are bicep modules
- foreach (var resource in model.Resources)
- {
- if (resource.TryGetLastAnnotation(out var targetAnnotation) &&
- targetAnnotation.DeploymentTarget is AzureBicepResource br)
- {
- var moduleDirectory = outputDirectory.CreateSubdirectory(resource.Name);
-
- var modulePath = Path.Combine(moduleDirectory.FullName, $"{resource.Name}.bicep");
-
- var file = br.GetBicepTemplateFile();
-
- File.Copy(file.Path, modulePath, true);
-
- // TODO: Resolve parameters for the module and
- // handle flowing outputs from other modules
-
- foreach (var parameter in br.Parameters)
- {
- Visit(parameter.Value, v =>
- {
- if (v is BicepOutputReference bo)
- {
- // Any bicep output reference needs to be propagated to the top level
- outputs[bo.ValueExpression] = bo;
- }
- });
- }
- }
- }
-
- // Add parameters to the infrastructure
- foreach (var (_, pp) in parameterMap)
- {
- infra.Add(pp);
- }
-
- // Add the parameters to the infrastructure
- infra.Add(tags);
-
- // Add the resource group to the infrastructure
- infra.Add(rg);
-
- // Add the modules to the infrastructure
- foreach (var (_, module) in moduleMap)
- {
- // Add the module to the infrastructure
- infra.Add(module);
- }
-
- // Add the outputs to the infrastructure
- foreach (var (_, output) in outputs)
- {
- var module = moduleMap[output.Resource];
-
- var identifier = Infrastructure.NormalizeBicepIdentifier($"{output.Resource.Name}_{output.Name}");
-
- var bicepOutput = new ProvisioningOutput(identifier, typeof(string))
- {
- Value = GetOutputs(module, output.Name)
- };
-
- infra.Add(bicepOutput);
- }
-
- SaveToDisk(outputDirectory.FullName, infra);
-
- return Task.CompletedTask;
- }
-
- private static void Visit(object? value, Action