Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1182198
wip
ArcturusZhang Sep 18, 2025
04672bd
a few updates
ArcturusZhang Sep 18, 2025
b40c484
some updates
ArcturusZhang Sep 22, 2025
12c9ffd
some updates around the list
ArcturusZhang Sep 23, 2025
423d65b
change the tobicepexpression as an extension method
ArcturusZhang Sep 23, 2025
be423ba
clean up and implement the same for dictionary
ArcturusZhang Sep 23, 2025
575bb33
refine test cases
ArcturusZhang Sep 23, 2025
f713804
update api
ArcturusZhang Sep 24, 2025
2846d9b
add the check for setters and all methods that modify the content of …
ArcturusZhang Sep 24, 2025
0dc3c35
update code and source code
ArcturusZhang Sep 24, 2025
d1283bc
resolve comments
ArcturusZhang Oct 16, 2025
ba3c526
Update sdk/provisioning/Azure.Provisioning/src/BicepListOfT.cs
ArcturusZhang Oct 16, 2025
56e29aa
Update sdk/provisioning/Azure.Provisioning/src/BicepDictionaryOfT.cs
ArcturusZhang Oct 16, 2025
034d4ce
Merge remote-tracking branch 'origin/main' into bicep-value-expressio…
ArcturusZhang Oct 16, 2025
bd80a2c
add a new test
ArcturusZhang Oct 16, 2025
5784933
add a note in test case
ArcturusZhang Oct 16, 2025
0856df2
update lists. dictionary pending. tests pending
ArcturusZhang Oct 20, 2025
e94b796
export api
ArcturusZhang Oct 20, 2025
1ab64cf
fix test cases and implement for dictionary
ArcturusZhang Oct 20, 2025
38fb6ef
refactor and clean up
ArcturusZhang Oct 20, 2025
9719eb9
export api
ArcturusZhang Oct 20, 2025
0247988
update document
ArcturusZhang Oct 20, 2025
c09229e
update readme
ArcturusZhang Oct 20, 2025
5c69e1d
Merge remote-tracking branch 'origin/main' into bicep-value-expressio…
ArcturusZhang Oct 21, 2025
592a239
Merge remote-tracking branch 'origin/main' into bicep-value-expressio…
ArcturusZhang Oct 29, 2025
ff2e3a1
fixes after merge
ArcturusZhang Oct 29, 2025
7a5866e
Merge remote-tracking branch 'origin/main' into bicep-value-expressio…
ArcturusZhang Oct 31, 2025
42b9cea
resolving comments
ArcturusZhang Oct 31, 2025
bee38bb
update readmes
ArcturusZhang Oct 31, 2025
0aa8d5b
update readme
ArcturusZhang Oct 31, 2025
15184cd
Merge remote-tracking branch 'origin/main' into bicep-value-expressio…
ArcturusZhang Oct 31, 2025
55fa72c
update readme
ArcturusZhang Oct 31, 2025
1654b93
fix spellcheck
ArcturusZhang Oct 31, 2025
ac9c28d
update snippets
ArcturusZhang Oct 31, 2025
ea1cedc
Merge remote-tracking branch 'origin/main' into bicep-value-expressio…
ArcturusZhang Oct 31, 2025
0e39b85
Update sdk/provisioning/Azure.Provisioning/src/Utilities/BicepValueHe…
ArcturusZhang Nov 3, 2025
415732b
resolve comments
ArcturusZhang Nov 3, 2025
f8adac4
Merge remote-tracking branch 'origin/main' into bicep-value-expressio…
ArcturusZhang Nov 3, 2025
f1c98ff
Merge remote-tracking branch 'origin/main' into bicep-value-expressio…
ArcturusZhang Nov 7, 2025
346b6b9
update changelog
ArcturusZhang Nov 7, 2025
0f926c4
fix changelog
ArcturusZhang Nov 10, 2025
a2bae80
Merge remote-tracking branch 'origin/main' into bicep-value-expressio…
ArcturusZhang Nov 10, 2025
32c1636
remove the dependency of azure.provisioning.appservice
ArcturusZhang Nov 10, 2025
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
8 changes: 5 additions & 3 deletions sdk/provisioning/Azure.Provisioning/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
# Release History

## 1.4.0-beta.2 (Unreleased)
## 1.4.0-beta.2 (2025-11-10)

### Features Added

- Added extension method `BicepValueExtensions.ToBicepExpression` which converts any `IBicepValue` into `BicepExpression` to build up complex expressions in bicep. For more details, please refer to the documents in `README`.
- Added `BicepFunction.GetResourceId` corresponding to bicep built-in function `resourceId`.
- Added `BicepFunction.GetExtensionResourceId` corresponding to bicep built-in function `extensionResourceId`.

### Breaking Changes

### Bugs Fixed

- Enabled the ability to assign expressions into a property with type of a `ProvisionableConstruct` via low level APIs.
- Fixed exception when output variable has a type of array or object.
- Fixed bug when indexing output list or dictionary, a `KeyNotFoundException` was always thrown. ([#48491](https://github.com/Azure/azure-sdk-for-net/issues/48491))

### Other Changes

- Now collection types (`BicepList<T>` and `BicepDictionary<T>`) would be able to force to be empty. ([#53346](https://github.com/Azure/azure-sdk-for-net/issues/53346))

## 1.4.0-beta.1 (2025-09-03)

### Features Added
Expand Down
229 changes: 201 additions & 28 deletions sdk/provisioning/Azure.Provisioning/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,62 @@ dotnet add package Azure.Provisioning

## Key concepts

This library allows you to specify your infrastructure in a declarative style using dotnet. You can then use `azd` to deploy your infrastructure to Azure directly without needing to write or maintain `bicep` or arm templates.
This library allows you to specify your infrastructure in a declarative style using `dotnet`. You can then use `azd` to deploy your infrastructure to Azure directly without needing to write or maintain `bicep` or `arm` templates.

### Important Usage Guidelines

**Declarative Design Pattern**: `Azure.Provisioning` is designed for declarative infrastructure definition. Each resource and construct instance should represent a single infrastructure component. Avoid reusing the same instance across multiple properties or locations, as this can lead to unexpected behavior in the generated Bicep templates.

```C# Snippet:CreateSeparateInstances
// ✅ Create separate instances
StorageAccount storage1 = new(nameof(storage1))
{
Sku = new StorageSku { Name = StorageSkuName.StandardLrs }
};
StorageAccount storage2 = new(nameof(storage2))
{
Sku = new StorageSku { Name = StorageSkuName.StandardLrs }
};
```

<details>
<summary>❌ What NOT to do - Click to expand bad example</summary>

```C# Snippet:ReuseInstances
// ❌ DO NOT reuse the same instance
StorageSku sharedSku = new() { Name = StorageSkuName.StandardLrs };
StorageAccount storage1 = new(nameof(storage1)) { Sku = sharedSku }; // ❌ Bad
StorageAccount storage2 = new(nameof(storage2)) { Sku = sharedSku }; // ❌ Bad
```

This pattern can lead to incorrect Bicep expressions when you build expressions on them. Details could be found in [this section](#tobicepexpression-method).

</details>

**Safe Collection Access**: You can safely access any index in a `BicepList` or any key in a `BicepDictionary` without exceptions. This is **especially important when working with output properties** from Azure resources, where the actual data doesn't exist at design time but you need to create references for the generated Bicep template.

```C# Snippet:SafeCollectionAccess
// ✅ Accessing output properties safely - very common scenario
Infrastructure infra = new();
CognitiveServicesAccount aiServices = new("aiServices");
infra.Add(aiServices);

// Safe to access dictionary keys that exist in the deployed resource
// but not at design time - no KeyNotFoundException thrown
BicepValue<string> apiEndpoint = aiServices.Properties.Endpoints["Azure AI Model Inference API"];

// Works perfectly for building references in outputs
infra.Add(new ProvisioningOutput("connectionString", typeof(string))
{
Value = BicepFunction.Interpolate($"Endpoint={apiEndpoint.ToBicepExpression()}")
});
// Generates: output connectionString string = 'Endpoint=${aiServices.properties.endpoints['Azure AI Model Inference API']}'

// ⚠️ Note: Accessing .Value will still throw at runtime if the data doesn't exist
// BicepValue<string> actualValue = apiEndpoint.Value; // Would throw KeyNotFoundException at runtime
```

This feature resolves common scenarios where you need to reference nested properties or collection items as outputs.

### `BicepValue` types

Expand All @@ -33,8 +88,7 @@ This library allows you to specify your infrastructure in a declarative style us
- A Bicep expression that evaluates to type `T`
- An unset value (usually one should get this state from the property of a constructed resource/construct)

```csharp
// Literal value
```C# Snippet:ThreeKindsOfBicepValue
BicepValue<string> literalName = "my-storage-account";

// Expression value
Expand All @@ -49,23 +103,27 @@ BicepValue<string> unsetName = storageAccount.Name;
- A Bicep expression that evaluates to an array
- An unset list (usually one should get this state from the property of a constructed resource/construct)

```csharp
```C# Snippet:BicepListUsages
// Literal list
BicepList<string> tagNames = new() { "Environment", "Project", "Owner" };

// Expression list (referencing a parameter)
BicepList<string> dynamicTags = parameterReference;
// Modifying items
tagNames.Add("CostCenter"); // add an item
tagNames.Remove("Owner"); // remove an item
tagNames[0] = "Env"; // modify an item
tagNames.Clear(); // clear all items

// Adding items
tagNames.Add("CostCenter");
// Expression list (referencing a parameter)
ProvisioningParameter parameter = new(nameof(parameter), typeof(string[]));
BicepList<string> dynamicTags = parameter;
```

**`BicepDictionary<T>`** - Represents a key-value collection where values are `BicepValue<T>`:
- A dictionary of literal key-value pairs
- A Bicep expression that evaluates to an object
- An unset dictionary (usually one should get this state from the property of a constructed resource/construct)

```csharp
```C# Snippet:BicepDictionaryUsages
// Literal dictionary
BicepDictionary<string> tags = new()
{
Expand All @@ -74,31 +132,35 @@ BicepDictionary<string> tags = new()
["Owner"] = "DevTeam"
};

// Expression dictionary
BicepDictionary<string> dynamicTags = parameterReference;

// Accessing values
tags["CostCenter"] = "12345";

// Expression dictionary
ProvisioningParameter parameter = new(nameof(parameter), typeof(object));
BicepDictionary<string> dynamicTags = parameter;
```

#### Working with Azure Resources

**`ProvisionableResource`** - Base class for Azure resources that provides resource-specific functionality. Users typically work with specific resource types like `StorageAccount`, `VirtualMachine`, `AppService`, etc. An instance of type `ProvisionableResource` corresponds to a resource statement in `bicep` language.
**`ProvisionableResource`** - Base class for Azure resources that provides resource-specific functionality. Users typically work with specific resource types like `StorageAccount`, `VirtualNetwork`, `WebSite`, etc. An instance of type `ProvisionableResource` corresponds to a resource statement in `bicep` language.

**`ProvisionableConstruct`** - Base class for infrastructure components that group related properties and resources. Most users will work with concrete implementations like `StorageAccountSku`, `VirtualNetworkIPConfiguration`, etc. An instance of type `ProvisionableConstruct` usually corresponds to an object definition statement in `bicep` language.
**`ProvisionableConstruct`** - Base class for infrastructure components that group related properties and resources. Most users will work with concrete implementations like `StorageAccountSku`, `VirtualNetworkEncryption`, etc. An instance of type `ProvisionableConstruct` usually corresponds to an object definition statement in `bicep` language.

Here's how you use the provided Azure resource classes:

```csharp
```C# Snippet:WorkingWithAzureResources
// Define parameters for dynamic configuration
ProvisioningParameter location = new(nameof(location), typeof(string));
ProvisioningParameter environment = new(nameof(environment), typeof(string));
// Create a storage account with BicepValue properties
StorageAccount storage = new("myStorage", StorageAccount.ResourceVersions.V2023_01_01)
StorageAccount myStorage = new(nameof(myStorage), StorageAccount.ResourceVersions.V2023_01_01)
{
// Set literal values
Name = "mystorageaccount",
Kind = StorageKind.StorageV2,

// Use BicepValue for dynamic configuration
Location = locationParameter, // Reference a parameter
Location = location, // Reference a parameter

// Configure nested properties
Sku = new StorageSku
Expand All @@ -110,25 +172,136 @@ StorageAccount storage = new("myStorage", StorageAccount.ResourceVersions.V2023_
Tags = new BicepDictionary<string>
{
["Environment"] = "Production",
["Project"] = environmentParameter // Mix literal and dynamic values
["Project"] = environment // Mix literal and dynamic values
}
};

// Access output properties (these are BicepValue<T> that reference the deployed resource)
BicepValue<string> storageAccountId = storage.Id;
BicepValue<Uri> primaryBlobEndpoint = storage.PrimaryEndpoints.BlobUri;
// Access output properties and use them in output (these are BicepValue<T> that reference the deployed resource)
ProvisioningOutput storageAccountId = new(nameof(storageAccountId), typeof(string))
{
Value = myStorage.Id
};
ProvisioningOutput primaryBlobEndpoint = new(nameof(primaryBlobEndpoint), typeof(string))
{
Value = myStorage.PrimaryEndpoints.BlobUri
};
```

### `ToBicepExpression` Method

The `ToBicepExpression()` extension method allows you to create references to resource properties and values for use in Bicep expressions. This is essential when you need to reference one resource's properties in another resource or build dynamic configuration strings.

```C# Snippet:CommonUseCases
// Create a storage account
StorageAccount storage = new(nameof(storage), StorageAccount.ResourceVersions.V2023_01_01)
{
Name = "mystorageaccount",
Kind = StorageKind.StorageV2
};

// Reference the storage account name in a connection string
BicepValue<string> connectionString = BicepFunction.Interpolate(
$"AccountName={storage.Name.ToBicepExpression()};EndpointSuffix=core.windows.net"
);
// this would produce: 'AccountName=${storage.name};EndpointSuffix=core.windows.net'
// If we do not call ToBicepExpression()
BicepValue<string> nonExpressionConnectionString =
BicepFunction.Interpolate(
$"AccountName={storage.Name};EndpointSuffix=core.windows.net"
);
// this would produce: 'AccountName=mystorageaccount;EndpointSuffix=core.windows.net'
```

Use `ToBicepExpression()` whenever you need to reference a resource property or value in Bicep expressions, function calls, or when building dynamic configuration values.

#### Important Notes

**NamedProvisionableConstruct Requirement**:

`ToBicepExpression()` requires that the value can be traced back through a chain of properties to a root `NamedProvisionableConstruct`. The method recursively traverses up the property ownership chain until it finds a `NamedProvisionableConstruct` at the root.

**Types that qualify as root `NamedProvisionableConstruct`:**
- **Azure resources** (like `StorageAccount`, `CognitiveServicesAccount`, etc.) - these inherit from `ProvisionableResource`
- **Infrastructure components** like:
- `ProvisioningParameter` - input parameters to your template
- `ProvisioningOutput` - output values from your template
- `ProvisioningVariable` - variables within your template
- `ModuleImport` - imported modules

**How the traversal works:**
- ✅ `storage.Name` - direct property of `StorageAccount` (a `NamedProvisionableConstruct`)
- ✅ `storage.Sku.Name` - `Sku` is a property of `StorageAccount`, `Name` is a property of `Sku`
- ✅ `storage.Properties.Encryption.Services.Blob.Enabled` - any depth is supported as long as it traces back to `StorageAccount`
- ✅ `storage.Tags[0]` - collection element where the collection (`Tags`) is a property of `StorageAccount`
- ✅ `storage.NetworkRuleSet.VirtualNetworkRules[0].Action` - element of a list property, then accessing a property of that element
- ❌ `new StorageSku().Name` - standalone `StorageSku` has no traceable path to a `NamedProvisionableConstruct`

This restriction exists because the generated Bicep expression needs an identifier to make it syntax correct (e.g., `storage.sku.name` or `param.someProperty.value`).

```C# Snippet:NamedProvisionableConstructRequirement
// ✅ Works - calling from a property of StorageAccount which inherits from ProvisionableResource
StorageAccount storage = new("myStorage");
BicepExpression nameRef = storage.Name.ToBicepExpression(); // Works

// ✅ Works - calling from a ProvisioningParameter
ProvisioningParameter param = new("myParam", typeof(string));
BicepExpression paramRef = param.ToBicepExpression(); // Works

// ❌ Throws exception - StorageSku is just a ProvisionableConstruct (not a NamedProvisionableConstruct)
StorageSku sku = new() { Name = StorageSkuName.StandardLrs };
// BicepExpression badRef = sku.Name.ToBicepExpression(); // Throws exception
// ✅ Works - if you assign it to another NamedProvisionableConstruct first
storage.Sku = sku;
BicepExpression goodRef = storage.Sku.Name.ToBicepExpression(); // Works
```

**Why Instance Sharing Fails**:

As mentioned in the [Declarative Design Pattern](#important-usage-guidelines) section, sharing the same construct instance across multiple properties leads to problems with `ToBicepExpression()`. Here's the correct approach and what happens when you don't follow it:

// Reference properties in other resources
var appService = new AppService("myApp")
**The correct approach:**

```C# Snippet:InstanceSharingCorrect
// ✅ GOOD: Create separate instances with the same values
StorageAccount storage1 = new("storage1")
{
// Reference the storage account's connection string
ConnectionStrings = new BicepDictionary<string>
{
["Storage"] = BicepFunction.Interpolate($"DefaultEndpointsProtocol=https;AccountName={storage.Name};AccountKey={storage.GetKeys().Value[0].Value}")
}
Sku = new StorageSku { Name = StorageSkuName.StandardLrs }
};
StorageAccount storage2 = new("storage2")
{
Sku = new StorageSku { Name = StorageSkuName.StandardLrs }
};

// Each has its own StorageSku instance
// Bicep expressions work correctly and unambiguously:
BicepExpression sku1Ref = storage1.Sku.Name.ToBicepExpression(); // "${storage1.sku.name}"
BicepExpression sku2Ref = storage2.Sku.Name.ToBicepExpression(); // "${storage2.sku.name}"
```

**What NOT to do and why it fails:**

```C# Snippet:InstanceSharingProblem
// ❌ BAD: Sharing the same StorageSku instance
StorageSku sharedSku = new() { Name = StorageSkuName.StandardLrs };

StorageAccount storage1 = new("storage1") { Sku = sharedSku };
StorageAccount storage2 = new("storage2") { Sku = sharedSku };

// Now both storage accounts reference the SAME StorageSku object
// This creates ambiguity when building Bicep expressions:

// ❌ PROBLEM: Which storage account should this reference?
// storage1.sku.name or storage2.sku.name?
BicepExpression skuNameRef = sharedSku.Name.ToBicepExpression(); // Confusing and unpredictable!

// The system can't determine whether this should generate:
// - "${storage1.sku.name}"
// - "${storage2.sku.name}"
// This leads to incorrect or unpredictable Bicep output.
```

**Key takeaway:** Each construct instance must have a single, unambiguous path back to its owning `NamedProvisionableConstruct`. Sharing instances breaks this requirement and makes Bicep reference generation impossible.

## Examples

### Create Basic Infrastructure
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ public partial class BicepDictionary<T> : Azure.Provisioning.BicepValue, System.
public BicepDictionary() { }
public BicepDictionary(System.Collections.Generic.IDictionary<string, Azure.Provisioning.BicepValue<T>>? values) { }
public int Count { get { throw null; } }
public override bool IsEmpty { get { throw null; } }
public bool IsReadOnly { get { throw null; } }
public Azure.Provisioning.BicepValue<T> this[string key] { get { throw null; } set { } }
public System.Collections.Generic.ICollection<string> Keys { get { throw null; } }
Expand Down Expand Up @@ -41,7 +40,6 @@ public partial class BicepList<T> : Azure.Provisioning.BicepValue, System.Collec
public BicepList() { }
public BicepList(System.Collections.Generic.IList<Azure.Provisioning.BicepValue<T>>? values) { }
public int Count { get { throw null; } }
public override bool IsEmpty { get { throw null; } }
public bool IsReadOnly { get { throw null; } }
public Azure.Provisioning.BicepValue<T> this[int index] { get { throw null; } set { } }
public void Add(Azure.Provisioning.BicepValue<T> item) { }
Expand Down Expand Up @@ -81,6 +79,7 @@ void Azure.Provisioning.IBicepValue.SetReadOnly() { }
}
public static partial class BicepValueExtensions
{
public static Azure.Provisioning.Expressions.BicepExpression ToBicepExpression(this Azure.Provisioning.IBicepValue bicepValue) { throw null; }
public static T Unwrap<T>(this Azure.Provisioning.BicepValue<T> value) where T : Azure.Provisioning.Primitives.ProvisionableConstruct, new() { throw null; }
}
public enum BicepValueKind
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ public partial class BicepDictionary<T> : Azure.Provisioning.BicepValue, System.
public BicepDictionary() { }
public BicepDictionary(System.Collections.Generic.IDictionary<string, Azure.Provisioning.BicepValue<T>>? values) { }
public int Count { get { throw null; } }
public override bool IsEmpty { get { throw null; } }
public bool IsReadOnly { get { throw null; } }
public Azure.Provisioning.BicepValue<T> this[string key] { get { throw null; } set { } }
public System.Collections.Generic.ICollection<string> Keys { get { throw null; } }
Expand Down Expand Up @@ -41,7 +40,6 @@ public partial class BicepList<T> : Azure.Provisioning.BicepValue, System.Collec
public BicepList() { }
public BicepList(System.Collections.Generic.IList<Azure.Provisioning.BicepValue<T>>? values) { }
public int Count { get { throw null; } }
public override bool IsEmpty { get { throw null; } }
public bool IsReadOnly { get { throw null; } }
public Azure.Provisioning.BicepValue<T> this[int index] { get { throw null; } set { } }
public void Add(Azure.Provisioning.BicepValue<T> item) { }
Expand Down Expand Up @@ -81,6 +79,7 @@ void Azure.Provisioning.IBicepValue.SetReadOnly() { }
}
public static partial class BicepValueExtensions
{
public static Azure.Provisioning.Expressions.BicepExpression ToBicepExpression(this Azure.Provisioning.IBicepValue bicepValue) { throw null; }
public static T Unwrap<T>(this Azure.Provisioning.BicepValue<T> value) where T : Azure.Provisioning.Primitives.ProvisionableConstruct, new() { throw null; }
}
public enum BicepValueKind
Expand Down
Loading