Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 59 additions & 12 deletions src/Aspire.Hosting.Azure.Network/AzureSubnetResource.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,35 +14,68 @@ namespace Aspire.Hosting.Azure;
/// <summary>
/// Represents an Azure Subnet resource.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="subnetName">The subnet name.</param>
/// <param name="addressPrefix">The address prefix for the subnet.</param>
/// <param name="parent">The parent Virtual Network resource.</param>
/// <remarks>
/// Use <see cref="AzureProvisioningResourceExtensions.ConfigureInfrastructure{T}(ApplicationModel.IResourceBuilder{T}, Action{AzureResourceInfrastructure})"/> to configure specific <see cref="Azure.Provisioning"/> properties.
/// </remarks>
public class AzureSubnetResource(string name, string subnetName, string addressPrefix, AzureVirtualNetworkResource parent)
: Resource(name), IResourceWithParent<AzureVirtualNetworkResource>
public class AzureSubnetResource : Resource, IResourceWithParent<AzureVirtualNetworkResource>
{
// Backing field holds either string or ParameterResource
private readonly object _addressPrefix;

/// <summary>
/// Initializes a new instance of the <see cref="AzureSubnetResource"/> class.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="subnetName">The subnet name.</param>
/// <param name="addressPrefix">The address prefix for the subnet.</param>
/// <param name="parent">The parent Virtual Network resource.</param>
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));
}

/// <summary>
/// Initializes a new instance of the <see cref="AzureSubnetResource"/> class with a parameterized address prefix.
/// </summary>
/// <param name="name">The name of the resource.</param>
/// <param name="subnetName">The subnet name.</param>
/// <param name="addressPrefix">The parameter resource containing the address prefix for the subnet.</param>
/// <param name="parent">The parent Virtual Network resource.</param>
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));
}

/// <summary>
/// Gets the subnet name.
/// </summary>
public string SubnetName { get; } = ThrowIfNullOrEmpty(subnetName);
public string SubnetName { get; }

/// <summary>
/// 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 <c>null</c> if the address prefix is provided via a <see cref="ParameterResource"/>.
/// </summary>
public string AddressPrefix { get; } = ThrowIfNullOrEmpty(addressPrefix);
public string? AddressPrefix => _addressPrefix as string;

/// <summary>
/// Gets the parameter resource containing the address prefix for the subnet, or <c>null</c> if the address prefix is provided as a literal string.
/// </summary>
public ParameterResource? AddressPrefixParameter => _addressPrefix as ParameterResource;

/// <summary>
/// Gets the subnet Id output reference.
/// </summary>
public BicepOutputReference Id => new($"{Infrastructure.NormalizeBicepIdentifier(Name)}_Id", parent);
public BicepOutputReference Id => new($"{Infrastructure.NormalizeBicepIdentifier(Name)}_Id", Parent);

/// <summary>
/// Gets the parent Azure Virtual Network resource.
/// </summary>
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);
Expand All @@ -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);
Expand Down
40 changes: 40 additions & 0 deletions src/Aspire.Hosting.Azure.Network/AzureVirtualNetworkExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,46 @@ public static IResourceBuilder<AzureSubnetResource> AddSubnet(

var subnet = new AzureSubnetResource(name, subnetName, addressPrefix, builder.Resource);

return AddSubnetCore(builder, subnet);
}

/// <summary>
/// Adds an Azure Subnet to the Virtual Network with a parameterized address prefix.
/// </summary>
/// <param name="builder">The Virtual Network resource builder.</param>
/// <param name="name">The name of the subnet resource.</param>
/// <param name="addressPrefix">The parameter resource containing the address prefix for the subnet (e.g., "10.0.1.0/24").</param>
/// <param name="subnetName">The subnet name in Azure. If null, the resource name is used.</param>
/// <returns>A reference to the <see cref="IResourceBuilder{AzureSubnetResource}"/>.</returns>
/// <example>
/// This example adds a subnet with a parameterized address prefix:
/// <code>
/// var subnetPrefix = builder.AddParameter("subnetPrefix");
/// var vnet = builder.AddAzureVirtualNetwork("vnet");
/// var subnet = vnet.AddSubnet("my-subnet", subnetPrefix);
/// </code>
/// </example>
public static IResourceBuilder<AzureSubnetResource> AddSubnet(
this IResourceBuilder<AzureVirtualNetworkResource> builder,
[ResourceName] string name,
IResourceBuilder<ParameterResource> 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<AzureSubnetResource> AddSubnetCore(
IResourceBuilder<AzureVirtualNetworkResource> builder,
AzureSubnetResource subnet)
{
builder.Resource.Subnets.Add(subnet);

if (builder.ApplicationBuilder.ExecutionContext.IsRunMode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,4 +126,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");
}
}
Original file line number Diff line number Diff line change
@@ -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
Loading