diff --git a/Aspire.sln b/Aspire.sln
index ad79e3c7f1..2f417f04c8 100644
--- a/Aspire.sln
+++ b/Aspire.sln
@@ -307,6 +307,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProxylessEndToEnd.ApiServic
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ProxylessEndToEnd.AppHost", "playground\ProxylessEndToEnd\ProxylessEndToEnd.AppHost\ProxylessEndToEnd.AppHost.csproj", "{0244203D-7491-4414-9C88-10BFED9C5B2D}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "cdk", "cdk", "{C3F48531-87D9-4E52-90AC-715A3E55751A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CdkSample.AppHost", "playground\cdk\CdkSample.AppHost\CdkSample.AppHost.csproj", "{A357411A-5909-4A49-9519-12A935F84395}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CdkSample.ApiService", "playground\cdk\CdkSample.ApiService\CdkSample.ApiService.csproj", "{4601F5A2-E445-41B2-9C1F-2CE016642E62}"
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CustomResources", "CustomResources", "{867A00A7-AF8E-4396-9583-982FBB31762C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CustomResources.AppHost", "playground\CustomResources\CustomResources.AppHost\CustomResources.AppHost.csproj", "{4231B6F1-1110-4992-A727-8F1176A47440}"
@@ -789,6 +794,14 @@ Global
{0244203D-7491-4414-9C88-10BFED9C5B2D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0244203D-7491-4414-9C88-10BFED9C5B2D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0244203D-7491-4414-9C88-10BFED9C5B2D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A357411A-5909-4A49-9519-12A935F84395}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A357411A-5909-4A49-9519-12A935F84395}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A357411A-5909-4A49-9519-12A935F84395}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A357411A-5909-4A49-9519-12A935F84395}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4601F5A2-E445-41B2-9C1F-2CE016642E62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4601F5A2-E445-41B2-9C1F-2CE016642E62}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4601F5A2-E445-41B2-9C1F-2CE016642E62}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4601F5A2-E445-41B2-9C1F-2CE016642E62}.Release|Any CPU.Build.0 = Release|Any CPU
{4231B6F1-1110-4992-A727-8F1176A47440}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4231B6F1-1110-4992-A727-8F1176A47440}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4231B6F1-1110-4992-A727-8F1176A47440}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -955,6 +968,9 @@ Global
{6723830C-7E39-4709-836B-17E5EE426751} = {A4F7DD00-59FE-4754-B0CF-3CC8C0856E17}
{5345A33F-B845-4F2D-A934-F9D73327B1CE} = {A4F7DD00-59FE-4754-B0CF-3CC8C0856E17}
{0244203D-7491-4414-9C88-10BFED9C5B2D} = {9C30FFD6-2262-45E7-B010-24B30E0433C2}
+ {C3F48531-87D9-4E52-90AC-715A3E55751A} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
+ {A357411A-5909-4A49-9519-12A935F84395} = {C3F48531-87D9-4E52-90AC-715A3E55751A}
+ {4601F5A2-E445-41B2-9C1F-2CE016642E62} = {C3F48531-87D9-4E52-90AC-715A3E55751A}
{867A00A7-AF8E-4396-9583-982FBB31762C} = {D173887B-AF42-4576-B9C1-96B9E9B3D9C0}
{4231B6F1-1110-4992-A727-8F1176A47440} = {867A00A7-AF8E-4396-9583-982FBB31762C}
EndGlobalSection
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 91e2474a54..72df8e855f 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -19,9 +19,11 @@
+
-
-
+
+
+
diff --git a/NuGet.config b/NuGet.config
index e8f26f0e63..960384ec28 100644
--- a/NuGet.config
+++ b/NuGet.config
@@ -17,9 +17,15 @@
+
-
+
+
+
+
+
+
diff --git a/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.csproj b/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.csproj
new file mode 100644
index 0000000000..88b35ed33a
--- /dev/null
+++ b/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.csproj
@@ -0,0 +1,13 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
diff --git a/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.http b/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.http
new file mode 100644
index 0000000000..59fcd09ca5
--- /dev/null
+++ b/playground/cdk/CdkSample.ApiService/CdkSample.ApiService.http
@@ -0,0 +1,6 @@
+@CosmosEndToEnd.ApiService_HostAddress = http://localhost:5193
+
+GET {{SqlServerEndToEnd.ApiService_HostAddress}}/weatherforecast/
+Accept: application/json
+
+###
diff --git a/playground/cdk/CdkSample.ApiService/Program.cs b/playground/cdk/CdkSample.ApiService/Program.cs
new file mode 100644
index 0000000000..643f6ce15e
--- /dev/null
+++ b/playground/cdk/CdkSample.ApiService/Program.cs
@@ -0,0 +1,15 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.AddServiceDefaults();
+
+var app = builder.Build();
+
+app.MapGet("/", (IConfiguration config) =>
+{
+ return $"TABLE_URI is: {config["TABLE_URI"]}";
+});
+
+app.Run();
diff --git a/playground/cdk/CdkSample.ApiService/Properties/launchSettings.json b/playground/cdk/CdkSample.ApiService/Properties/launchSettings.json
new file mode 100644
index 0000000000..f7bf310e7a
--- /dev/null
+++ b/playground/cdk/CdkSample.ApiService/Properties/launchSettings.json
@@ -0,0 +1,14 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5180",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/playground/cdk/CdkSample.ApiService/appsettings.Development.json b/playground/cdk/CdkSample.ApiService/appsettings.Development.json
new file mode 100644
index 0000000000..0c208ae918
--- /dev/null
+++ b/playground/cdk/CdkSample.ApiService/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/playground/cdk/CdkSample.ApiService/appsettings.json b/playground/cdk/CdkSample.ApiService/appsettings.json
new file mode 100644
index 0000000000..10f68b8c8b
--- /dev/null
+++ b/playground/cdk/CdkSample.ApiService/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/playground/cdk/CdkSample.AppHost/CdkSample.AppHost.csproj b/playground/cdk/CdkSample.AppHost/CdkSample.AppHost.csproj
new file mode 100644
index 0000000000..40ab6d9383
--- /dev/null
+++ b/playground/cdk/CdkSample.AppHost/CdkSample.AppHost.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ true
+ 44b9bf37-1892-4852-8b5f-153e3ac5d24c
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/playground/cdk/CdkSample.AppHost/Directory.Build.props b/playground/cdk/CdkSample.AppHost/Directory.Build.props
new file mode 100644
index 0000000000..b9b39c05e8
--- /dev/null
+++ b/playground/cdk/CdkSample.AppHost/Directory.Build.props
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/playground/cdk/CdkSample.AppHost/Directory.Build.targets b/playground/cdk/CdkSample.AppHost/Directory.Build.targets
new file mode 100644
index 0000000000..b7ba77268f
--- /dev/null
+++ b/playground/cdk/CdkSample.AppHost/Directory.Build.targets
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/playground/cdk/CdkSample.AppHost/Program.cs b/playground/cdk/CdkSample.AppHost/Program.cs
new file mode 100644
index 0000000000..27354cf9fc
--- /dev/null
+++ b/playground/cdk/CdkSample.AppHost/Program.cs
@@ -0,0 +1,34 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Azure.Provisioning.Storage;
+using Azure.ResourceManager.Storage.Models;
+
+var builder = DistributedApplication.CreateBuilder(args);
+builder.AddAzureProvisioning();
+
+var sku = builder.AddParameter("storagesku");
+
+var construct1 = builder.AddAzureConstruct("construct1", (construct) =>
+{
+ var account = construct.AddStorageAccount(
+ name: "bob",
+ kind: StorageKind.BlobStorage,
+ sku: StorageSkuName.StandardLrs
+ );
+ account.AssignParameter(a => a.Sku.Name, construct.AddParameter(sku));
+
+ account.AddOutput(data => data.PrimaryEndpoints.TableUri, "tableUri", isSecure: true);
+});
+
+builder.AddProject("api")
+ .WithEnvironment("TABLE_URI", construct1.GetOutput("tableUri"));
+
+// This project is only added in playground projects to support development/debugging
+// of the dashboard. It is not required in end developer code. Comment out this code
+// to test end developer dashboard launch experience. Refer to Directory.Build.props
+// for the path to the dashboard binary (defaults to the Aspire.Dashboard bin output
+// in the artifacts dir).
+builder.AddProject(KnownResourceNames.AspireDashboard);
+
+builder.Build().Run();
diff --git a/playground/cdk/CdkSample.AppHost/Properties/launchSettings.json b/playground/cdk/CdkSample.AppHost/Properties/launchSettings.json
new file mode 100644
index 0000000000..32514f6378
--- /dev/null
+++ b/playground/cdk/CdkSample.AppHost/Properties/launchSettings.json
@@ -0,0 +1,29 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:15888",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16175",
+ "DOTNET_ASPIRE_SHOW_DASHBOARD_RESOURCES": "true"
+ }
+ },
+ "generate-manifest": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "dotnetRunMessages": true,
+ "commandLineArgs": "--publisher manifest --output-path aspire-manifest.json",
+ "applicationUrl": "http://localhost:15888",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "DOTNET_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:16175"
+ }
+ }
+ }
+}
diff --git a/playground/cdk/CdkSample.AppHost/appsettings.Development.json b/playground/cdk/CdkSample.AppHost/appsettings.Development.json
new file mode 100644
index 0000000000..0c208ae918
--- /dev/null
+++ b/playground/cdk/CdkSample.AppHost/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/playground/cdk/CdkSample.AppHost/appsettings.json b/playground/cdk/CdkSample.AppHost/appsettings.json
new file mode 100644
index 0000000000..a40f92dadf
--- /dev/null
+++ b/playground/cdk/CdkSample.AppHost/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "Aspire.Hosting.Dcp": "Warning"
+ }
+ },
+ "Parameters": {
+ "val": "value from config"
+ }
+}
diff --git a/playground/cdk/CdkSample.AppHost/aspire-manifest.json b/playground/cdk/CdkSample.AppHost/aspire-manifest.json
new file mode 100644
index 0000000000..556354092c
--- /dev/null
+++ b/playground/cdk/CdkSample.AppHost/aspire-manifest.json
@@ -0,0 +1,41 @@
+{
+ "resources": {
+ "storagesku": {
+ "type": "parameter.v0",
+ "value": "{storagesku.inputs.value}",
+ "inputs": {
+ "value": {
+ "type": "string"
+ }
+ }
+ },
+ "construct1": {
+ "type": "azure.bicep.v0",
+ "path": "construct1.module.bicep",
+ "params": {
+ "storagesku": "{storagesku.value}"
+ }
+ },
+ "api": {
+ "type": "project.v0",
+ "path": "../CdkSample.ApiService/CdkSample.ApiService.csproj",
+ "env": {
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EXCEPTION_LOG_ATTRIBUTES": "true",
+ "OTEL_DOTNET_EXPERIMENTAL_OTLP_EMIT_EVENT_LOG_ATTRIBUTES": "true",
+ "TABLE_URI": "{construct1.outputs.tableUri}"
+ },
+ "bindings": {
+ "http": {
+ "scheme": "http",
+ "protocol": "tcp",
+ "transport": "http"
+ },
+ "https": {
+ "scheme": "https",
+ "protocol": "tcp",
+ "transport": "http"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/playground/cdk/CdkSample.AppHost/construct1.module.bicep b/playground/cdk/CdkSample.AppHost/construct1.module.bicep
new file mode 100644
index 0000000000..d1892b40ed
--- /dev/null
+++ b/playground/cdk/CdkSample.AppHost/construct1.module.bicep
@@ -0,0 +1,21 @@
+targetScope = 'resourceGroup'
+
+@description('')
+param location string = resourceGroup().location
+
+@description('')
+param storagesku string
+
+
+resource storageAccount_unUi1Obb4 'Microsoft.Storage/storageAccounts@2022-09-01' = {
+ name: toLower(take(concat('bob', uniqueString(resourceGroup().id)), 24))
+ location: location
+ sku: {
+ name: storagesku
+ }
+ kind: 'StorageV2'
+ properties: {
+ }
+}
+
+output tableUri string = storageAccount_unUi1Obb4.properties.primaryEndpoints.table
diff --git a/src/Aspire.Hosting.Azure.Provisioning/Provisioners/BicepProvisioner.cs b/src/Aspire.Hosting.Azure.Provisioning/Provisioners/BicepProvisioner.cs
index 0373bd52ba..604b65c0a6 100644
--- a/src/Aspire.Hosting.Azure.Provisioning/Provisioners/BicepProvisioner.cs
+++ b/src/Aspire.Hosting.Azure.Provisioning/Provisioners/BicepProvisioner.cs
@@ -240,8 +240,11 @@ private static void PopulateWellKnownParameters(AzureBicepResource resource, Pro
resource.Parameters.Remove(AzureBicepResource.KnownParameters.LogAnalyticsWorkspaceId);
}
- // Always specify the location
- resource.Parameters[AzureBicepResource.KnownParameters.Location] = context.Location.Name;
+ if (resource.Parameters.TryGetValue(AzureBicepResource.KnownParameters.Location, out var location) && location is null)
+ {
+ // Always specify the location
+ resource.Parameters[AzureBicepResource.KnownParameters.Location] = context.Location.Name;
+ }
}
private static async Task ExecuteCommand(ProcessSpec processSpec)
diff --git a/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj b/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj
index a38080183b..898ddf5116 100644
--- a/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj
+++ b/src/Aspire.Hosting.Azure/Aspire.Hosting.Azure.csproj
@@ -18,6 +18,7 @@
+
diff --git a/src/Aspire.Hosting.Azure/AzureBicepResource.cs b/src/Aspire.Hosting.Azure/AzureBicepResource.cs
index ecde2fda91..1217f773da 100644
--- a/src/Aspire.Hosting.Azure/AzureBicepResource.cs
+++ b/src/Aspire.Hosting.Azure/AzureBicepResource.cs
@@ -22,7 +22,7 @@ public class AzureBicepResource(string name, string? templateFile = null, string
{
internal string? TemplateFile { get; } = templateFile;
- internal string? TemplateString { get; } = templateString;
+ internal string? TemplateString { get; set; } = templateString;
internal string? TemplateResourceName { get; } = templateResourceName;
@@ -48,7 +48,7 @@ public class AzureBicepResource(string name, string? templateFile = null, string
/// A boolean that determines if the file should be deleted on disposal of the .
/// A that represents the bicep file.
///
- public BicepTemplateFile GetBicepTemplateFile(string? directory = null, bool deleteTemporaryFileOnDispose = true)
+ public virtual BicepTemplateFile GetBicepTemplateFile(string? directory = null, bool deleteTemporaryFileOnDispose = true)
{
// Throw if multiple template sources are specified
if (TemplateFile is not null && (TemplateString is not null || TemplateResourceName is not null))
diff --git a/src/Aspire.Hosting.Azure/AzureConstructResource.cs b/src/Aspire.Hosting.Azure/AzureConstructResource.cs
new file mode 100644
index 0000000000..44f1a3a092
--- /dev/null
+++ b/src/Aspire.Hosting.Azure/AzureConstructResource.cs
@@ -0,0 +1,140 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Hosting.ApplicationModel;
+using Aspire.Hosting.Azure;
+using Azure.Provisioning;
+
+namespace Aspire.Hosting;
+
+///
+/// An Aspire resource that supports use of Azure Provisioning APIs to create Azure resources.
+///
+///
+///
+public class AzureConstructResource(string name, Action configureConstruct) : AzureBicepResource(name, templateFile: $"{name}.module.bicep")
+{
+ ///
+ /// Callback for configuring construct.
+ ///
+ public Action ConfigureConstruct { get; } = configureConstruct;
+
+ ///
+ public override BicepTemplateFile GetBicepTemplateFile(string? directory = null, bool deleteTemporaryFileOnDispose = true)
+ {
+ var configuration = new Configuration()
+ {
+ UsePromptMode = true
+ };
+
+ var resourceModuleConstruct = new ResourceModuleConstruct(this, configuration);
+
+ foreach (var aspireParameter in this.Parameters)
+ {
+ var constructParameter = new Parameter(aspireParameter.Key);
+ resourceModuleConstruct.AddParameter(constructParameter);
+ }
+
+ ConfigureConstruct(resourceModuleConstruct);
+
+ var generationPath = Directory.CreateTempSubdirectory("aspire").FullName;
+ resourceModuleConstruct.Build(generationPath);
+
+ var moduleSourcePath = Path.Combine(generationPath, "main.bicep");
+ var moduleDestinationPath = Path.Combine(directory ?? generationPath, $"{Name}.module.bicep");
+ File.Copy(moduleSourcePath, moduleDestinationPath!, true);
+
+ return new BicepTemplateFile(moduleDestinationPath, directory is null);
+ }
+}
+
+///
+/// Extensions for working with and related types.
+///
+public static class AzureConstructResourceExtensions
+{
+ ///
+ /// Adds an Azure construct resource to the application model.
+ ///
+ /// The distributed application builder.
+ /// The name of the resource being added.
+ /// A callback used to configure the construct resource.
+ ///
+ public static IResourceBuilder AddAzureConstruct(this IDistributedApplicationBuilder builder, string name, Action configureConstruct)
+ {
+ var resource = new AzureConstructResource(name, configureConstruct);
+ return builder.AddResource(resource)
+ .WithManifestPublishingCallback(resource.WriteToManifest);
+ }
+
+ ///
+ /// Adds a parameter to the Azure construct resource based on an Aspire parameter.
+ ///
+ /// The Azure construct resource.
+ /// The Aspire parameter resource builder.
+ ///
+ public static Parameter AddParameter(this ResourceModuleConstruct resourceModuleConstruct, IResourceBuilder parameterResourceBuilder)
+ {
+ return resourceModuleConstruct.AddParameter(parameterResourceBuilder.Resource.Name, parameterResourceBuilder);
+ }
+
+ ///
+ /// Adds a parameter to the Azure construct resource based on an Aspire parameter.
+ ///
+ /// The Azure construct resource.
+ /// The name to be used for the Azure construct parameter.
+ /// The Aspire parameter resource builder.
+ ///
+ public static Parameter AddParameter(this ResourceModuleConstruct resourceModuleConstruct, string name, IResourceBuilder parameterResourceBuilder)
+ {
+ // Ensure the parameter is added to the Aspire resource.
+ resourceModuleConstruct.Resource.Parameters.Add(name, parameterResourceBuilder);
+
+ var parameter = new Parameter(name, isSecure: parameterResourceBuilder.Resource.Secret);
+ resourceModuleConstruct.AddParameter(parameter);
+ return parameter;
+ }
+
+ ///
+ /// Adds a parameter to the Azure construct resource based on an
+ ///
+ /// The Azure construct resource.
+ /// The Aspire Bicep output reference.
+ ///
+ public static Parameter AddParameter(this ResourceModuleConstruct resourceModuleConstruct, BicepOutputReference outputReference)
+ {
+ return resourceModuleConstruct.AddParameter(outputReference.Name, outputReference);
+ }
+
+ ///
+ /// Adds a parameter to the Azure construct resource based on an
+ ///
+ /// The Azure construct resource.
+ /// The name to be used for the Azure construct parameter.
+ /// The Aspire Bicep output reference.
+ ///
+ public static Parameter AddParameter(this ResourceModuleConstruct resourceModuleConstruct, string name, BicepOutputReference outputReference)
+ {
+ resourceModuleConstruct.Resource.Parameters.Add(name, outputReference);
+
+ var parameter = new Parameter(name);
+ resourceModuleConstruct.AddParameter(parameter);
+ return parameter;
+ }
+}
+
+///
+/// An Azure Provisioning construct which represents the root Bicep module that is generated for an Azure construct resource.
+///
+public class ResourceModuleConstruct : Infrastructure
+{
+ internal ResourceModuleConstruct(AzureConstructResource resource, Configuration configuration) : base(constructScope: ConstructScope.ResourceGroup, tenantId: Guid.Empty, subscriptionId: Guid.Empty, envName: "temp", configuration: configuration)
+ {
+ Resource = resource;
+ }
+
+ ///
+ /// The Azure cosntruct resource that this resource module construct represents.
+ ///
+ public AzureConstructResource Resource { get; }
+}
diff --git a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs
index 70de9877fb..c178fe75b4 100644
--- a/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs
+++ b/tests/Aspire.Hosting.Tests/Azure/AzureBicepResourceTests.cs
@@ -5,6 +5,8 @@
using System.Text.Json.Nodes;
using Aspire.Hosting.Azure;
using Aspire.Hosting.Utils;
+using Azure.Provisioning.Storage;
+using Azure.ResourceManager.Storage.Models;
using Xunit;
namespace Aspire.Hosting.Tests.Azure;
@@ -184,6 +186,54 @@ public async Task WithReferenceAppInsightsSetsEnvironmentVariable()
Assert.Equal("myinstrumentationkey", config["APPLICATIONINSIGHTS_CONNECTION_STRING"]);
}
+ [Fact]
+ public async Task AddAzureConstructGenertesCorrectManifestEntry()
+ {
+ var builder = DistributedApplication.CreateBuilder();
+ var construct1 = builder.AddAzureConstruct("construct1", (construct) =>
+ {
+ var storage = construct.AddStorageAccount(
+ kind: StorageKind.StorageV2,
+ sku: StorageSkuName.StandardLrs
+ );
+ storage.AddOutput(sa => sa.Name, "storageAccountName");
+ });
+
+ var manifest = await ManifestUtils.GetManifest(construct1.Resource);
+ Assert.Equal("azure.bicep.v0", manifest["type"]?.ToString());
+ Assert.Equal("construct1.module.bicep", manifest["path"]?.ToString());
+ }
+
+ // TODO: This test to be reenabled once we figure out what is going on in CDK
+ // around parameters being injected twice.
+ //[Fact]
+ //public void AddParameterOnResourceModuleConstructPopulatesParametersEverywhere()
+ //{
+ // var builder = DistributedApplication.CreateBuilder();
+ // builder.Configuration["Parameters:skuName"] = "Standard_ZRS";
+
+ // var skuName = builder.AddParameter("skuName");
+
+ // ResourceModuleConstruct? moduleConstruct = null;
+ // var construct1 = builder.AddAzureConstruct("construct1", (construct) =>
+ // {
+ // var storage = construct.AddStorageAccount(
+ // kind: StorageKind.StorageV2,
+ // sku: StorageSkuName.StandardLrs
+ // );
+ // storage.AssignParameter(sa => sa.Sku.Name, construct.AddParameter(skuName));
+ // moduleConstruct = construct;
+ // });
+
+ // var manifest = ManifestUtils.GetManifest(construct1.Resource);
+
+ // Assert.NotNull(moduleConstruct);
+ // var constructParameters = moduleConstruct.GetParameters(false).ToDictionary(p => p.Name);
+ // Assert.True(constructParameters.ContainsKey("skuName"));
+ // Assert.Equal(skuName.Resource, construct1.Resource.Parameters["skuName"]);
+ // Assert.Equal("{skuName.value}", manifest["params"]?["skuName"]?.ToString());
+ //}
+
[Fact]
public async Task PublishAsRedisPublishesRedisAsAzureRedis()
{
diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs
index a5bfd9799a..eb085b0d4a 100644
--- a/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs
+++ b/tests/Aspire.Hosting.Tests/DistributedApplicationTests.cs
@@ -469,7 +469,7 @@ public async Task KubernetesHasResourceNameForContainersAndExes()
}
}
- await foreach(var resource in s.WatchAsync(cancellationToken: token))
+ await foreach (var resource in s.WatchAsync(cancellationToken: token))
{
Assert.True(resource.Item2.Metadata.Annotations.TryGetValue(Executable.ResourceNameAnnotation, out var value));
if (expectedExeResources.Contains(value))