diff --git a/src/Aspire.Hosting.Azure.Network/AzureSubnetResource.cs b/src/Aspire.Hosting.Azure.Network/AzureSubnetResource.cs
index ae962a7d3cf..7717c6d3674 100644
--- a/src/Aspire.Hosting.Azure.Network/AzureSubnetResource.cs
+++ b/src/Aspire.Hosting.Azure.Network/AzureSubnetResource.cs
@@ -1,6 +1,7 @@
// 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.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using Aspire.Hosting.ApplicationModel;
@@ -13,35 +14,68 @@ namespace Aspire.Hosting.Azure;
///
/// Represents an Azure Subnet resource.
///
-/// The name of the resource.
-/// The subnet name.
-/// The address prefix for the subnet.
-/// The parent Virtual Network resource.
///
/// Use to configure specific properties.
///
-public class AzureSubnetResource(string name, string subnetName, string addressPrefix, AzureVirtualNetworkResource parent)
- : Resource(name), IResourceWithParent
+public class AzureSubnetResource : Resource, IResourceWithParent
{
+ // Backing field holds either string or ParameterResource
+ private readonly object _addressPrefix;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The name of the resource.
+ /// The subnet name.
+ /// The address prefix for the subnet.
+ /// The parent Virtual Network resource.
+ public AzureSubnetResource(string name, string subnetName, string addressPrefix, AzureVirtualNetworkResource parent)
+ : base(name)
+ {
+ SubnetName = ThrowIfNullOrEmpty(subnetName);
+ _addressPrefix = ThrowIfNullOrEmpty(addressPrefix);
+ Parent = parent ?? throw new ArgumentNullException(nameof(parent));
+ }
+
+ ///
+ /// Initializes a new instance of the class with a parameterized address prefix.
+ ///
+ /// The name of the resource.
+ /// The subnet name.
+ /// The parameter resource containing the address prefix for the subnet.
+ /// The parent Virtual Network resource.
+ public AzureSubnetResource(string name, string subnetName, ParameterResource addressPrefix, AzureVirtualNetworkResource parent)
+ : base(name)
+ {
+ SubnetName = ThrowIfNullOrEmpty(subnetName);
+ _addressPrefix = addressPrefix ?? throw new ArgumentNullException(nameof(addressPrefix));
+ Parent = parent ?? throw new ArgumentNullException(nameof(parent));
+ }
+
///
/// Gets the subnet name.
///
- public string SubnetName { get; } = ThrowIfNullOrEmpty(subnetName);
+ public string SubnetName { get; }
///
- /// Gets the address prefix for the subnet (e.g., "10.0.1.0/24").
+ /// Gets the address prefix for the subnet (e.g., "10.0.1.0/24"), or null if the address prefix is provided via a .
///
- public string AddressPrefix { get; } = ThrowIfNullOrEmpty(addressPrefix);
+ public string? AddressPrefix => _addressPrefix as string;
+
+ ///
+ /// Gets the parameter resource containing the address prefix for the subnet, or null if the address prefix is provided as a literal string.
+ ///
+ public ParameterResource? AddressPrefixParameter => _addressPrefix as ParameterResource;
///
/// Gets the subnet Id output reference.
///
- public BicepOutputReference Id => new($"{Infrastructure.NormalizeBicepIdentifier(Name)}_Id", parent);
+ public BicepOutputReference Id => new($"{Infrastructure.NormalizeBicepIdentifier(Name)}_Id", Parent);
///
/// Gets the parent Azure Virtual Network resource.
///
- public AzureVirtualNetworkResource Parent { get; } = parent ?? throw new ArgumentNullException(nameof(parent));
+ public AzureVirtualNetworkResource Parent { get; }
private static string ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
=> !string.IsNullOrEmpty(argument) ? argument : throw new ArgumentNullException(paramName);
@@ -54,9 +88,22 @@ internal SubnetResource ToProvisioningEntity(AzureResourceInfrastructure infra,
var subnet = new SubnetResource(Infrastructure.NormalizeBicepIdentifier(Name))
{
Name = SubnetName,
- AddressPrefix = AddressPrefix,
};
+ // Set the address prefix from either the literal string or the parameter
+ if (_addressPrefix is string addressPrefix)
+ {
+ subnet.AddressPrefix = addressPrefix;
+ }
+ else if (_addressPrefix is ParameterResource addressPrefixParameter)
+ {
+ subnet.AddressPrefix = addressPrefixParameter.AsProvisioningParameter(infra);
+ }
+ else
+ {
+ throw new UnreachableException("AddressPrefix must be set either as a string or a ParameterResource.");
+ }
+
if (dependsOn is not null)
{
subnet.DependsOn.Add(dependsOn);
diff --git a/src/Aspire.Hosting.Azure.Network/AzureVirtualNetworkExtensions.cs b/src/Aspire.Hosting.Azure.Network/AzureVirtualNetworkExtensions.cs
index 3aad3dd2485..9f464e95b8d 100644
--- a/src/Aspire.Hosting.Azure.Network/AzureVirtualNetworkExtensions.cs
+++ b/src/Aspire.Hosting.Azure.Network/AzureVirtualNetworkExtensions.cs
@@ -38,8 +38,46 @@ public static IResourceBuilder AddAzureVirtualNetwo
builder.AddAzureProvisioning();
- AzureVirtualNetworkResource resource = new(name, ConfigureVirtualNetwork);
+ AzureVirtualNetworkResource resource = new(name, ConfigureVirtualNetwork, addressPrefix);
+ return AddAzureVirtualNetworkCore(builder, resource);
+ }
+
+ ///
+ /// Adds an Azure Virtual Network resource to the application model with a parameterized address prefix.
+ ///
+ /// The builder for the distributed application.
+ /// The name of the Azure Virtual Network resource.
+ /// The parameter resource containing the address prefix for the virtual network (e.g., "10.0.0.0/16").
+ /// A reference to the .
+ ///
+ /// This example creates a virtual network with a parameterized address prefix:
+ ///
+ /// var vnetPrefix = builder.AddParameter("vnetPrefix");
+ /// var vnet = builder.AddAzureVirtualNetwork("vnet", vnetPrefix);
+ /// var subnet = vnet.AddSubnet("pe-subnet", "10.0.1.0/24");
+ ///
+ ///
+ public static IResourceBuilder AddAzureVirtualNetwork(
+ this IDistributedApplicationBuilder builder,
+ [ResourceName] string name,
+ IResourceBuilder addressPrefix)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentException.ThrowIfNullOrEmpty(name);
+ ArgumentNullException.ThrowIfNull(addressPrefix);
+
+ builder.AddAzureProvisioning();
+
+ AzureVirtualNetworkResource resource = new(name, ConfigureVirtualNetwork, addressPrefix.Resource);
+
+ return AddAzureVirtualNetworkCore(builder, resource);
+ }
+
+ private static IResourceBuilder AddAzureVirtualNetworkCore(
+ IDistributedApplicationBuilder builder,
+ AzureVirtualNetworkResource resource)
+ {
if (builder.ExecutionContext.IsRunMode)
{
// In run mode, we don't want to add the resource to the builder.
@@ -47,57 +85,69 @@ public static IResourceBuilder AddAzureVirtualNetwo
}
return builder.AddResource(resource);
+ }
- void ConfigureVirtualNetwork(AzureResourceInfrastructure infra)
- {
- var vnet = AzureProvisioningResource.CreateExistingOrNewProvisionableResource(infra,
- (identifier, name) =>
+ private static void ConfigureVirtualNetwork(AzureResourceInfrastructure infra)
+ {
+ var azureResource = (AzureVirtualNetworkResource)infra.AspireResource;
+
+ var vnet = AzureProvisioningResource.CreateExistingOrNewProvisionableResource(infra,
+ (identifier, name) =>
+ {
+ var resource = VirtualNetwork.FromExisting(identifier);
+ resource.Name = name;
+ return resource;
+ },
+ (infrastructure) =>
+ {
+ var vnet = new VirtualNetwork(infrastructure.AspireResource.GetBicepIdentifier())
{
- var resource = VirtualNetwork.FromExisting(identifier);
- resource.Name = name;
- return resource;
- },
- (infrastructure) =>
+ Tags = { { "aspire-resource-name", infrastructure.AspireResource.Name } }
+ };
+
+ // Set the address prefix from either the literal string or the parameter
+ if (azureResource.AddressPrefix is { } addressPrefix)
{
- var vnet = new VirtualNetwork(infrastructure.AspireResource.GetBicepIdentifier())
+ vnet.AddressSpace = new VirtualNetworkAddressSpace()
{
- AddressSpace = new VirtualNetworkAddressSpace()
- {
- AddressPrefixes = { addressPrefix ?? "10.0.0.0/16" }
- },
- Tags = { { "aspire-resource-name", infrastructure.AspireResource.Name } }
+ AddressPrefixes = { addressPrefix }
};
+ }
+ else if (azureResource.AddressPrefixParameter is { } addressPrefixParameter)
+ {
+ vnet.AddressSpace = new VirtualNetworkAddressSpace()
+ {
+ AddressPrefixes = { addressPrefixParameter.AsProvisioningParameter(infrastructure) }
+ };
+ }
- return vnet;
- });
-
- var azureResource = (AzureVirtualNetworkResource)infra.AspireResource;
+ return vnet;
+ });
- // Add subnets
- if (azureResource.Subnets.Count > 0)
+ // Add subnets
+ if (azureResource.Subnets.Count > 0)
+ {
+ // Chain subnet provisioning to ensure deployment doesn't fail
+ // due to parallel creation of subnets within the VNet.
+ ProvisionableResource? dependsOn = null;
+ foreach (var subnet in azureResource.Subnets)
{
- // Chain subnet provisioning to ensure deployment doesn't fail
- // due to parallel creation of subnets within the VNet.
- ProvisionableResource? dependsOn = null;
- foreach (var subnet in azureResource.Subnets)
- {
- var cdkSubnet = subnet.ToProvisioningEntity(infra, dependsOn);
- cdkSubnet.Parent = vnet;
- infra.Add(cdkSubnet);
+ var cdkSubnet = subnet.ToProvisioningEntity(infra, dependsOn);
+ cdkSubnet.Parent = vnet;
+ infra.Add(cdkSubnet);
- dependsOn = cdkSubnet;
- }
+ dependsOn = cdkSubnet;
}
+ }
- // Output the VNet ID for references
- infra.Add(new ProvisioningOutput("id", typeof(string))
- {
- Value = vnet.Id
- });
+ // Output the VNet ID for references
+ infra.Add(new ProvisioningOutput("id", typeof(string))
+ {
+ Value = vnet.Id
+ });
- // We need to output name so it can be referenced by others.
- infra.Add(new ProvisioningOutput("name", typeof(string)) { Value = vnet.Name });
- }
+ // We need to output name so it can be referenced by others.
+ infra.Add(new ProvisioningOutput("name", typeof(string)) { Value = vnet.Name });
}
///
@@ -129,6 +179,46 @@ public static IResourceBuilder AddSubnet(
var subnet = new AzureSubnetResource(name, subnetName, addressPrefix, builder.Resource);
+ return AddSubnetCore(builder, subnet);
+ }
+
+ ///
+ /// Adds an Azure Subnet to the Virtual Network with a parameterized address prefix.
+ ///
+ /// The Virtual Network resource builder.
+ /// The name of the subnet resource.
+ /// The parameter resource containing the address prefix for the subnet (e.g., "10.0.1.0/24").
+ /// The subnet name in Azure. If null, the resource name is used.
+ /// A reference to the .
+ ///
+ /// This example adds a subnet with a parameterized address prefix:
+ ///
+ /// var subnetPrefix = builder.AddParameter("subnetPrefix");
+ /// var vnet = builder.AddAzureVirtualNetwork("vnet");
+ /// var subnet = vnet.AddSubnet("my-subnet", subnetPrefix);
+ ///
+ ///
+ public static IResourceBuilder AddSubnet(
+ this IResourceBuilder builder,
+ [ResourceName] string name,
+ IResourceBuilder addressPrefix,
+ string? subnetName = null)
+ {
+ ArgumentNullException.ThrowIfNull(builder);
+ ArgumentException.ThrowIfNullOrEmpty(name);
+ ArgumentNullException.ThrowIfNull(addressPrefix);
+
+ subnetName ??= name;
+
+ var subnet = new AzureSubnetResource(name, subnetName, addressPrefix.Resource, builder.Resource);
+
+ return AddSubnetCore(builder, subnet);
+ }
+
+ private static IResourceBuilder AddSubnetCore(
+ IResourceBuilder builder,
+ AzureSubnetResource subnet)
+ {
builder.Resource.Subnets.Add(subnet);
if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
diff --git a/src/Aspire.Hosting.Azure.Network/AzureVirtualNetworkResource.cs b/src/Aspire.Hosting.Azure.Network/AzureVirtualNetworkResource.cs
index 203393fc420..e253c7c32a5 100644
--- a/src/Aspire.Hosting.Azure.Network/AzureVirtualNetworkResource.cs
+++ b/src/Aspire.Hosting.Azure.Network/AzureVirtualNetworkResource.cs
@@ -1,6 +1,7 @@
// 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 Azure.Provisioning.Network;
using Azure.Provisioning.Primitives;
@@ -14,8 +15,23 @@ namespace Aspire.Hosting.Azure;
public class AzureVirtualNetworkResource(string name, Action configureInfrastructure)
: AzureProvisioningResource(name, configureInfrastructure)
{
+ private const string DefaultAddressPrefix = "10.0.0.0/16";
+
+ // Backing field holds either string or ParameterResource
+ private readonly object _addressPrefix = DefaultAddressPrefix;
+
internal List Subnets { get; } = [];
+ ///
+ /// Gets the address prefix for the virtual network (e.g., "10.0.0.0/16"), or null if the address prefix is provided via a .
+ ///
+ public string? AddressPrefix => _addressPrefix as string;
+
+ ///
+ /// Gets the parameter resource containing the address prefix for the virtual network, or null if the address prefix is provided as a literal string.
+ ///
+ public ParameterResource? AddressPrefixParameter => _addressPrefix as ParameterResource;
+
///
/// Gets the "id" output reference from the Azure Virtual Network resource.
///
@@ -26,6 +42,31 @@ public class AzureVirtualNetworkResource(string name, Action
public BicepOutputReference NameOutput => new("name", this);
+ ///
+ /// Initializes a new instance of the class with a string address prefix.
+ ///
+ /// The name of the resource.
+ /// Callback to configure the Azure Virtual Network resource.
+ /// The address prefix for the virtual network (e.g., "10.0.0.0/16").
+ public AzureVirtualNetworkResource(string name, Action configureInfrastructure, string? addressPrefix)
+ : this(name, configureInfrastructure)
+ {
+ _addressPrefix = addressPrefix ?? DefaultAddressPrefix;
+ }
+
+ ///
+ /// Initializes a new instance of the class with a parameterized address prefix.
+ ///
+ /// The name of the resource.
+ /// Callback to configure the Azure Virtual Network resource.
+ /// The parameter resource containing the address prefix for the virtual network.
+ public AzureVirtualNetworkResource(string name, Action configureInfrastructure, ParameterResource addressPrefix)
+ : this(name, configureInfrastructure)
+ {
+ ArgumentNullException.ThrowIfNull(addressPrefix);
+ _addressPrefix = addressPrefix;
+ }
+
///
public override ProvisionableResource AddAsExistingResource(AzureResourceInfrastructure infra)
{
diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureVirtualNetworkExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureVirtualNetworkExtensionsTests.cs
index e0b9a5db7c9..470e252ad89 100644
--- a/tests/Aspire.Hosting.Azure.Tests/AzureVirtualNetworkExtensionsTests.cs
+++ b/tests/Aspire.Hosting.Azure.Tests/AzureVirtualNetworkExtensionsTests.cs
@@ -30,6 +30,36 @@ public void AddAzureVirtualNetwork_WithCustomAddressPrefix()
Assert.NotNull(vnet);
Assert.Equal("myvnet", vnet.Resource.Name);
+ Assert.Equal("10.1.0.0/16", vnet.Resource.AddressPrefix);
+ Assert.Null(vnet.Resource.AddressPrefixParameter);
+ }
+
+ [Fact]
+ public void AddAzureVirtualNetwork_WithParameterResource_CreatesResource()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
+
+ var vnetPrefixParam = builder.AddParameter("vnetPrefix");
+ var vnet = builder.AddAzureVirtualNetwork("myvnet", vnetPrefixParam);
+
+ Assert.NotNull(vnet);
+ Assert.Equal("myvnet", vnet.Resource.Name);
+ Assert.Null(vnet.Resource.AddressPrefix);
+ Assert.Same(vnetPrefixParam.Resource, vnet.Resource.AddressPrefixParameter);
+ }
+
+ [Fact]
+ public async Task AddAzureVirtualNetwork_WithParameterResource_GeneratesBicep()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
+
+ var vnetPrefixParam = builder.AddParameter("vnetPrefix");
+ var vnet = builder.AddAzureVirtualNetwork("myvnet", vnetPrefixParam);
+
+ var manifest = await AzureManifestUtils.GetManifestWithBicep(vnet.Resource);
+
+ await Verify(manifest.BicepText, extension: "bicep")
+ .UseMethodName("AddAzureVirtualNetwork_WithParameterResource_GeneratesBicep");
}
[Fact]
@@ -126,4 +156,50 @@ public void WithDelegatedSubnet_AddsAnnotationsToSubnetAndTarget()
Assert.NotNull(delegationAnnotation);
Assert.Equal("Microsoft.App/environments", delegationAnnotation.ServiceName);
}
+
+ [Fact]
+ public void AddSubnet_WithParameterResource_CreatesSubnetResource()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
+
+ var addressPrefixParam = builder.AddParameter("subnetPrefix");
+ var vnet = builder.AddAzureVirtualNetwork("myvnet");
+ var subnet = vnet.AddSubnet("mysubnet", addressPrefixParam);
+
+ Assert.NotNull(subnet);
+ Assert.Equal("mysubnet", subnet.Resource.Name);
+ Assert.Equal("mysubnet", subnet.Resource.SubnetName);
+ Assert.Null(subnet.Resource.AddressPrefix);
+ Assert.Same(addressPrefixParam.Resource, subnet.Resource.AddressPrefixParameter);
+ Assert.Same(vnet.Resource, subnet.Resource.Parent);
+ }
+
+ [Fact]
+ public void AddSubnet_WithParameterResource_AndCustomSubnetName()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
+
+ var addressPrefixParam = builder.AddParameter("subnetPrefix");
+ var vnet = builder.AddAzureVirtualNetwork("myvnet");
+ var subnet = vnet.AddSubnet("mysubnet", addressPrefixParam, subnetName: "custom-subnet-name");
+
+ Assert.Equal("mysubnet", subnet.Resource.Name);
+ Assert.Equal("custom-subnet-name", subnet.Resource.SubnetName);
+ Assert.Null(subnet.Resource.AddressPrefix);
+ Assert.Same(addressPrefixParam.Resource, subnet.Resource.AddressPrefixParameter);
+ }
+
+ [Fact]
+ public async Task AddSubnet_WithParameterResource_GeneratesBicep()
+ {
+ using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
+
+ var addressPrefixParam = builder.AddParameter("subnetPrefix");
+ var vnet = builder.AddAzureVirtualNetwork("myvnet");
+ vnet.AddSubnet("mysubnet", addressPrefixParam);
+
+ var manifest = await AzureManifestUtils.GetManifestWithBicep(vnet.Resource);
+
+ await Verify(manifest.BicepText, extension: "bicep");
+ }
}
diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureVirtualNetworkExtensionsTests.AddAzureVirtualNetwork_WithParameterResource_GeneratesBicep.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureVirtualNetworkExtensionsTests.AddAzureVirtualNetwork_WithParameterResource_GeneratesBicep.verified.bicep
new file mode 100644
index 00000000000..e92965f0a9a
--- /dev/null
+++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureVirtualNetworkExtensionsTests.AddAzureVirtualNetwork_WithParameterResource_GeneratesBicep.verified.bicep
@@ -0,0 +1,23 @@
+@description('The location for the resource(s) to be deployed.')
+param location string = resourceGroup().location
+
+param vnetPrefix string
+
+resource myvnet 'Microsoft.Network/virtualNetworks@2025-05-01' = {
+ name: take('myvnet-${uniqueString(resourceGroup().id)}', 64)
+ properties: {
+ addressSpace: {
+ addressPrefixes: [
+ vnetPrefix
+ ]
+ }
+ }
+ location: location
+ tags: {
+ 'aspire-resource-name': 'myvnet'
+ }
+}
+
+output id string = myvnet.id
+
+output name string = myvnet.name
\ No newline at end of file
diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureVirtualNetworkExtensionsTests.AddSubnet_WithParameterResource_GeneratesBicep.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureVirtualNetworkExtensionsTests.AddSubnet_WithParameterResource_GeneratesBicep.verified.bicep
new file mode 100644
index 00000000000..94e76663c5d
--- /dev/null
+++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureVirtualNetworkExtensionsTests.AddSubnet_WithParameterResource_GeneratesBicep.verified.bicep
@@ -0,0 +1,33 @@
+@description('The location for the resource(s) to be deployed.')
+param location string = resourceGroup().location
+
+param subnetPrefix string
+
+resource myvnet 'Microsoft.Network/virtualNetworks@2025-05-01' = {
+ name: take('myvnet-${uniqueString(resourceGroup().id)}', 64)
+ properties: {
+ addressSpace: {
+ addressPrefixes: [
+ '10.0.0.0/16'
+ ]
+ }
+ }
+ location: location
+ tags: {
+ 'aspire-resource-name': 'myvnet'
+ }
+}
+
+resource mysubnet 'Microsoft.Network/virtualNetworks/subnets@2025-05-01' = {
+ name: 'mysubnet'
+ properties: {
+ addressPrefix: subnetPrefix
+ }
+ parent: myvnet
+}
+
+output mysubnet_Id string = mysubnet.id
+
+output id string = myvnet.id
+
+output name string = myvnet.name
\ No newline at end of file