Skip to content

Commit b69320c

Browse files
Introduce a bicep reference builder (#52742)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent cd4e110 commit b69320c

21 files changed

+2064
-171
lines changed

sdk/provisioning/Azure.Provisioning/CHANGELOG.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
# Release History
22

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

55
### Features Added
66

7+
- 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`.
78
- Added `BicepFunction.GetResourceId` corresponding to bicep built-in function `resourceId`.
89
- Added `BicepFunction.GetExtensionResourceId` corresponding to bicep built-in function `extensionResourceId`.
910

10-
### Breaking Changes
11-
1211
### Bugs Fixed
1312

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

1717
### Other Changes
1818

19+
- 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))
20+
1921
## 1.4.0-beta.1 (2025-09-03)
2022

2123
### Features Added

sdk/provisioning/Azure.Provisioning/README.md

Lines changed: 201 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,62 @@ dotnet add package Azure.Provisioning
2020

2121
## Key concepts
2222

23-
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.
23+
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.
24+
25+
### Important Usage Guidelines
26+
27+
**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.
28+
29+
```C# Snippet:CreateSeparateInstances
30+
// ✅ Create separate instances
31+
StorageAccount storage1 = new(nameof(storage1))
32+
{
33+
Sku = new StorageSku { Name = StorageSkuName.StandardLrs }
34+
};
35+
StorageAccount storage2 = new(nameof(storage2))
36+
{
37+
Sku = new StorageSku { Name = StorageSkuName.StandardLrs }
38+
};
39+
```
40+
41+
<details>
42+
<summary>❌ What NOT to do - Click to expand bad example</summary>
43+
44+
```C# Snippet:ReuseInstances
45+
// ❌ DO NOT reuse the same instance
46+
StorageSku sharedSku = new() { Name = StorageSkuName.StandardLrs };
47+
StorageAccount storage1 = new(nameof(storage1)) { Sku = sharedSku }; // ❌ Bad
48+
StorageAccount storage2 = new(nameof(storage2)) { Sku = sharedSku }; // ❌ Bad
49+
```
50+
51+
This pattern can lead to incorrect Bicep expressions when you build expressions on them. Details could be found in [this section](#tobicepexpression-method).
52+
53+
</details>
54+
55+
**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.
56+
57+
```C# Snippet:SafeCollectionAccess
58+
// ✅ Accessing output properties safely - very common scenario
59+
Infrastructure infra = new();
60+
CognitiveServicesAccount aiServices = new("aiServices");
61+
infra.Add(aiServices);
62+
63+
// Safe to access dictionary keys that exist in the deployed resource
64+
// but not at design time - no KeyNotFoundException thrown
65+
BicepValue<string> apiEndpoint = aiServices.Properties.Endpoints["Azure AI Model Inference API"];
66+
67+
// Works perfectly for building references in outputs
68+
infra.Add(new ProvisioningOutput("connectionString", typeof(string))
69+
{
70+
Value = BicepFunction.Interpolate($"Endpoint={apiEndpoint.ToBicepExpression()}")
71+
});
72+
// Generates: output connectionString string = 'Endpoint=${aiServices.properties.endpoints['Azure AI Model Inference API']}'
73+
74+
// ⚠️ Note: Accessing .Value will still throw at runtime if the data doesn't exist
75+
// BicepValue<string> actualValue = apiEndpoint.Value; // Would throw KeyNotFoundException at runtime
76+
```
77+
78+
This feature resolves common scenarios where you need to reference nested properties or collection items as outputs.
2479

2580
### `BicepValue` types
2681

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

36-
```csharp
37-
// Literal value
91+
```C# Snippet:ThreeKindsOfBicepValue
3892
BicepValue<string> literalName = "my-storage-account";
3993

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

52-
```csharp
106+
```C# Snippet:BicepListUsages
53107
// Literal list
54108
BicepList<string> tagNames = new() { "Environment", "Project", "Owner" };
55109

56-
// Expression list (referencing a parameter)
57-
BicepList<string> dynamicTags = parameterReference;
110+
// Modifying items
111+
tagNames.Add("CostCenter"); // add an item
112+
tagNames.Remove("Owner"); // remove an item
113+
tagNames[0] = "Env"; // modify an item
114+
tagNames.Clear(); // clear all items
58115
59-
// Adding items
60-
tagNames.Add("CostCenter");
116+
// Expression list (referencing a parameter)
117+
ProvisioningParameter parameter = new(nameof(parameter), typeof(string[]));
118+
BicepList<string> dynamicTags = parameter;
61119
```
62120

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

68-
```csharp
126+
```C# Snippet:BicepDictionaryUsages
69127
// Literal dictionary
70128
BicepDictionary<string> tags = new()
71129
{
@@ -74,31 +132,35 @@ BicepDictionary<string> tags = new()
74132
["Owner"] = "DevTeam"
75133
};
76134

77-
// Expression dictionary
78-
BicepDictionary<string> dynamicTags = parameterReference;
79-
80135
// Accessing values
81136
tags["CostCenter"] = "12345";
137+
138+
// Expression dictionary
139+
ProvisioningParameter parameter = new(nameof(parameter), typeof(object));
140+
BicepDictionary<string> dynamicTags = parameter;
82141
```
83142

84143
#### Working with Azure Resources
85144

86-
**`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.
145+
**`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.
87146

88-
**`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.
147+
**`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.
89148

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

92-
```csharp
151+
```C# Snippet:WorkingWithAzureResources
152+
// Define parameters for dynamic configuration
153+
ProvisioningParameter location = new(nameof(location), typeof(string));
154+
ProvisioningParameter environment = new(nameof(environment), typeof(string));
93155
// Create a storage account with BicepValue properties
94-
StorageAccount storage = new("myStorage", StorageAccount.ResourceVersions.V2023_01_01)
156+
StorageAccount myStorage = new(nameof(myStorage), StorageAccount.ResourceVersions.V2023_01_01)
95157
{
96158
// Set literal values
97159
Name = "mystorageaccount",
98160
Kind = StorageKind.StorageV2,
99161

100162
// Use BicepValue for dynamic configuration
101-
Location = locationParameter, // Reference a parameter
163+
Location = location, // Reference a parameter
102164
103165
// Configure nested properties
104166
Sku = new StorageSku
@@ -110,25 +172,136 @@ StorageAccount storage = new("myStorage", StorageAccount.ResourceVersions.V2023_
110172
Tags = new BicepDictionary<string>
111173
{
112174
["Environment"] = "Production",
113-
["Project"] = environmentParameter // Mix literal and dynamic values
175+
["Project"] = environment // Mix literal and dynamic values
114176
}
115177
};
116178

117-
// Access output properties (these are BicepValue<T> that reference the deployed resource)
118-
BicepValue<string> storageAccountId = storage.Id;
119-
BicepValue<Uri> primaryBlobEndpoint = storage.PrimaryEndpoints.BlobUri;
179+
// Access output properties and use them in output (these are BicepValue<T> that reference the deployed resource)
180+
ProvisioningOutput storageAccountId = new(nameof(storageAccountId), typeof(string))
181+
{
182+
Value = myStorage.Id
183+
};
184+
ProvisioningOutput primaryBlobEndpoint = new(nameof(primaryBlobEndpoint), typeof(string))
185+
{
186+
Value = myStorage.PrimaryEndpoints.BlobUri
187+
};
188+
```
189+
190+
### `ToBicepExpression` Method
191+
192+
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.
193+
194+
```C# Snippet:CommonUseCases
195+
// Create a storage account
196+
StorageAccount storage = new(nameof(storage), StorageAccount.ResourceVersions.V2023_01_01)
197+
{
198+
Name = "mystorageaccount",
199+
Kind = StorageKind.StorageV2
200+
};
201+
202+
// Reference the storage account name in a connection string
203+
BicepValue<string> connectionString = BicepFunction.Interpolate(
204+
$"AccountName={storage.Name.ToBicepExpression()};EndpointSuffix=core.windows.net"
205+
);
206+
// this would produce: 'AccountName=${storage.name};EndpointSuffix=core.windows.net'
207+
// If we do not call ToBicepExpression()
208+
BicepValue<string> nonExpressionConnectionString =
209+
BicepFunction.Interpolate(
210+
$"AccountName={storage.Name};EndpointSuffix=core.windows.net"
211+
);
212+
// this would produce: 'AccountName=mystorageaccount;EndpointSuffix=core.windows.net'
213+
```
214+
215+
Use `ToBicepExpression()` whenever you need to reference a resource property or value in Bicep expressions, function calls, or when building dynamic configuration values.
216+
217+
#### Important Notes
218+
219+
**NamedProvisionableConstruct Requirement**:
220+
221+
`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.
222+
223+
**Types that qualify as root `NamedProvisionableConstruct`:**
224+
- **Azure resources** (like `StorageAccount`, `CognitiveServicesAccount`, etc.) - these inherit from `ProvisionableResource`
225+
- **Infrastructure components** like:
226+
- `ProvisioningParameter` - input parameters to your template
227+
- `ProvisioningOutput` - output values from your template
228+
- `ProvisioningVariable` - variables within your template
229+
- `ModuleImport` - imported modules
230+
231+
**How the traversal works:**
232+
-`storage.Name` - direct property of `StorageAccount` (a `NamedProvisionableConstruct`)
233+
-`storage.Sku.Name` - `Sku` is a property of `StorageAccount`, `Name` is a property of `Sku`
234+
-`storage.Properties.Encryption.Services.Blob.Enabled` - any depth is supported as long as it traces back to `StorageAccount`
235+
-`storage.Tags[0]` - collection element where the collection (`Tags`) is a property of `StorageAccount`
236+
-`storage.NetworkRuleSet.VirtualNetworkRules[0].Action` - element of a list property, then accessing a property of that element
237+
-`new StorageSku().Name` - standalone `StorageSku` has no traceable path to a `NamedProvisionableConstruct`
238+
239+
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`).
240+
241+
```C# Snippet:NamedProvisionableConstructRequirement
242+
// ✅ Works - calling from a property of StorageAccount which inherits from ProvisionableResource
243+
StorageAccount storage = new("myStorage");
244+
BicepExpression nameRef = storage.Name.ToBicepExpression(); // Works
245+
246+
// ✅ Works - calling from a ProvisioningParameter
247+
ProvisioningParameter param = new("myParam", typeof(string));
248+
BicepExpression paramRef = param.ToBicepExpression(); // Works
249+
250+
// ❌ Throws exception - StorageSku is just a ProvisionableConstruct (not a NamedProvisionableConstruct)
251+
StorageSku sku = new() { Name = StorageSkuName.StandardLrs };
252+
// BicepExpression badRef = sku.Name.ToBicepExpression(); // Throws exception
253+
// ✅ Works - if you assign it to another NamedProvisionableConstruct first
254+
storage.Sku = sku;
255+
BicepExpression goodRef = storage.Sku.Name.ToBicepExpression(); // Works
256+
```
257+
258+
**Why Instance Sharing Fails**:
259+
260+
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:
120261

121-
// Reference properties in other resources
122-
var appService = new AppService("myApp")
262+
**The correct approach:**
263+
264+
```C# Snippet:InstanceSharingCorrect
265+
// ✅ GOOD: Create separate instances with the same values
266+
StorageAccount storage1 = new("storage1")
123267
{
124-
// Reference the storage account's connection string
125-
ConnectionStrings = new BicepDictionary<string>
126-
{
127-
["Storage"] = BicepFunction.Interpolate($"DefaultEndpointsProtocol=https;AccountName={storage.Name};AccountKey={storage.GetKeys().Value[0].Value}")
128-
}
268+
Sku = new StorageSku { Name = StorageSkuName.StandardLrs }
269+
};
270+
StorageAccount storage2 = new("storage2")
271+
{
272+
Sku = new StorageSku { Name = StorageSkuName.StandardLrs }
129273
};
274+
275+
// Each has its own StorageSku instance
276+
// Bicep expressions work correctly and unambiguously:
277+
BicepExpression sku1Ref = storage1.Sku.Name.ToBicepExpression(); // "${storage1.sku.name}"
278+
BicepExpression sku2Ref = storage2.Sku.Name.ToBicepExpression(); // "${storage2.sku.name}"
130279
```
131280

281+
**What NOT to do and why it fails:**
282+
283+
```C# Snippet:InstanceSharingProblem
284+
// ❌ BAD: Sharing the same StorageSku instance
285+
StorageSku sharedSku = new() { Name = StorageSkuName.StandardLrs };
286+
287+
StorageAccount storage1 = new("storage1") { Sku = sharedSku };
288+
StorageAccount storage2 = new("storage2") { Sku = sharedSku };
289+
290+
// Now both storage accounts reference the SAME StorageSku object
291+
// This creates ambiguity when building Bicep expressions:
292+
293+
// ❌ PROBLEM: Which storage account should this reference?
294+
// storage1.sku.name or storage2.sku.name?
295+
BicepExpression skuNameRef = sharedSku.Name.ToBicepExpression(); // Confusing and unpredictable!
296+
297+
// The system can't determine whether this should generate:
298+
// - "${storage1.sku.name}"
299+
// - "${storage2.sku.name}"
300+
// This leads to incorrect or unpredictable Bicep output.
301+
```
302+
303+
**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.
304+
132305
## Examples
133306

134307
### Create Basic Infrastructure

sdk/provisioning/Azure.Provisioning/api/Azure.Provisioning.net8.0.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ public partial class BicepDictionary<T> : Azure.Provisioning.BicepValue, System.
55
public BicepDictionary() { }
66
public BicepDictionary(System.Collections.Generic.IDictionary<string, Azure.Provisioning.BicepValue<T>>? values) { }
77
public int Count { get { throw null; } }
8-
public override bool IsEmpty { get { throw null; } }
98
public bool IsReadOnly { get { throw null; } }
109
public Azure.Provisioning.BicepValue<T> this[string key] { get { throw null; } set { } }
1110
public System.Collections.Generic.ICollection<string> Keys { get { throw null; } }
@@ -41,7 +40,6 @@ public partial class BicepList<T> : Azure.Provisioning.BicepValue, System.Collec
4140
public BicepList() { }
4241
public BicepList(System.Collections.Generic.IList<Azure.Provisioning.BicepValue<T>>? values) { }
4342
public int Count { get { throw null; } }
44-
public override bool IsEmpty { get { throw null; } }
4543
public bool IsReadOnly { get { throw null; } }
4644
public Azure.Provisioning.BicepValue<T> this[int index] { get { throw null; } set { } }
4745
public void Add(Azure.Provisioning.BicepValue<T> item) { }
@@ -81,6 +79,7 @@ void Azure.Provisioning.IBicepValue.SetReadOnly() { }
8179
}
8280
public static partial class BicepValueExtensions
8381
{
82+
public static Azure.Provisioning.Expressions.BicepExpression ToBicepExpression(this Azure.Provisioning.IBicepValue bicepValue) { throw null; }
8483
public static T Unwrap<T>(this Azure.Provisioning.BicepValue<T> value) where T : Azure.Provisioning.Primitives.ProvisionableConstruct, new() { throw null; }
8584
}
8685
public enum BicepValueKind

sdk/provisioning/Azure.Provisioning/api/Azure.Provisioning.netstandard2.0.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ public partial class BicepDictionary<T> : Azure.Provisioning.BicepValue, System.
55
public BicepDictionary() { }
66
public BicepDictionary(System.Collections.Generic.IDictionary<string, Azure.Provisioning.BicepValue<T>>? values) { }
77
public int Count { get { throw null; } }
8-
public override bool IsEmpty { get { throw null; } }
98
public bool IsReadOnly { get { throw null; } }
109
public Azure.Provisioning.BicepValue<T> this[string key] { get { throw null; } set { } }
1110
public System.Collections.Generic.ICollection<string> Keys { get { throw null; } }
@@ -41,7 +40,6 @@ public partial class BicepList<T> : Azure.Provisioning.BicepValue, System.Collec
4140
public BicepList() { }
4241
public BicepList(System.Collections.Generic.IList<Azure.Provisioning.BicepValue<T>>? values) { }
4342
public int Count { get { throw null; } }
44-
public override bool IsEmpty { get { throw null; } }
4543
public bool IsReadOnly { get { throw null; } }
4644
public Azure.Provisioning.BicepValue<T> this[int index] { get { throw null; } set { } }
4745
public void Add(Azure.Provisioning.BicepValue<T> item) { }
@@ -81,6 +79,7 @@ void Azure.Provisioning.IBicepValue.SetReadOnly() { }
8179
}
8280
public static partial class BicepValueExtensions
8381
{
82+
public static Azure.Provisioning.Expressions.BicepExpression ToBicepExpression(this Azure.Provisioning.IBicepValue bicepValue) { throw null; }
8483
public static T Unwrap<T>(this Azure.Provisioning.BicepValue<T> value) where T : Azure.Provisioning.Primitives.ProvisionableConstruct, new() { throw null; }
8584
}
8685
public enum BicepValueKind

0 commit comments

Comments
 (0)