diff --git a/sdk/provisioning/Azure.Provisioning/CHANGELOG.md b/sdk/provisioning/Azure.Provisioning/CHANGELOG.md index a7e93b755ec7..d77b315da83e 100644 --- a/sdk/provisioning/Azure.Provisioning/CHANGELOG.md +++ b/sdk/provisioning/Azure.Provisioning/CHANGELOG.md @@ -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` and `BicepDictionary`) 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 diff --git a/sdk/provisioning/Azure.Provisioning/README.md b/sdk/provisioning/Azure.Provisioning/README.md index d4b6257b2b9f..ce8782d75cca 100644 --- a/sdk/provisioning/Azure.Provisioning/README.md +++ b/sdk/provisioning/Azure.Provisioning/README.md @@ -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 } +}; +``` + +
+❌ What NOT to do - Click to expand bad example + +```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). + +
+ +**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 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 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 @@ -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 literalName = "my-storage-account"; // Expression value @@ -49,15 +103,19 @@ BicepValue 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 tagNames = new() { "Environment", "Project", "Owner" }; -// Expression list (referencing a parameter) -BicepList 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 dynamicTags = parameter; ``` **`BicepDictionary`** - Represents a key-value collection where values are `BicepValue`: @@ -65,7 +123,7 @@ tagNames.Add("CostCenter"); - 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 tags = new() { @@ -74,31 +132,35 @@ BicepDictionary tags = new() ["Owner"] = "DevTeam" }; -// Expression dictionary -BicepDictionary dynamicTags = parameterReference; - // Accessing values tags["CostCenter"] = "12345"; + +// Expression dictionary +ProvisioningParameter parameter = new(nameof(parameter), typeof(object)); +BicepDictionary 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 @@ -110,25 +172,136 @@ StorageAccount storage = new("myStorage", StorageAccount.ResourceVersions.V2023_ Tags = new BicepDictionary { ["Environment"] = "Production", - ["Project"] = environmentParameter // Mix literal and dynamic values + ["Project"] = environment // Mix literal and dynamic values } }; -// Access output properties (these are BicepValue that reference the deployed resource) -BicepValue storageAccountId = storage.Id; -BicepValue primaryBlobEndpoint = storage.PrimaryEndpoints.BlobUri; +// Access output properties and use them in output (these are BicepValue 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 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 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 - { - ["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 diff --git a/sdk/provisioning/Azure.Provisioning/api/Azure.Provisioning.net8.0.cs b/sdk/provisioning/Azure.Provisioning/api/Azure.Provisioning.net8.0.cs index c81afcb29c65..c8d661fbd57a 100644 --- a/sdk/provisioning/Azure.Provisioning/api/Azure.Provisioning.net8.0.cs +++ b/sdk/provisioning/Azure.Provisioning/api/Azure.Provisioning.net8.0.cs @@ -5,7 +5,6 @@ public partial class BicepDictionary : Azure.Provisioning.BicepValue, System. public BicepDictionary() { } public BicepDictionary(System.Collections.Generic.IDictionary>? values) { } public int Count { get { throw null; } } - public override bool IsEmpty { get { throw null; } } public bool IsReadOnly { get { throw null; } } public Azure.Provisioning.BicepValue this[string key] { get { throw null; } set { } } public System.Collections.Generic.ICollection Keys { get { throw null; } } @@ -41,7 +40,6 @@ public partial class BicepList : Azure.Provisioning.BicepValue, System.Collec public BicepList() { } public BicepList(System.Collections.Generic.IList>? values) { } public int Count { get { throw null; } } - public override bool IsEmpty { get { throw null; } } public bool IsReadOnly { get { throw null; } } public Azure.Provisioning.BicepValue this[int index] { get { throw null; } set { } } public void Add(Azure.Provisioning.BicepValue item) { } @@ -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(this Azure.Provisioning.BicepValue value) where T : Azure.Provisioning.Primitives.ProvisionableConstruct, new() { throw null; } } public enum BicepValueKind diff --git a/sdk/provisioning/Azure.Provisioning/api/Azure.Provisioning.netstandard2.0.cs b/sdk/provisioning/Azure.Provisioning/api/Azure.Provisioning.netstandard2.0.cs index b54a6db93c65..a99714fe203b 100644 --- a/sdk/provisioning/Azure.Provisioning/api/Azure.Provisioning.netstandard2.0.cs +++ b/sdk/provisioning/Azure.Provisioning/api/Azure.Provisioning.netstandard2.0.cs @@ -5,7 +5,6 @@ public partial class BicepDictionary : Azure.Provisioning.BicepValue, System. public BicepDictionary() { } public BicepDictionary(System.Collections.Generic.IDictionary>? values) { } public int Count { get { throw null; } } - public override bool IsEmpty { get { throw null; } } public bool IsReadOnly { get { throw null; } } public Azure.Provisioning.BicepValue this[string key] { get { throw null; } set { } } public System.Collections.Generic.ICollection Keys { get { throw null; } } @@ -41,7 +40,6 @@ public partial class BicepList : Azure.Provisioning.BicepValue, System.Collec public BicepList() { } public BicepList(System.Collections.Generic.IList>? values) { } public int Count { get { throw null; } } - public override bool IsEmpty { get { throw null; } } public bool IsReadOnly { get { throw null; } } public Azure.Provisioning.BicepValue this[int index] { get { throw null; } set { } } public void Add(Azure.Provisioning.BicepValue item) { } @@ -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(this Azure.Provisioning.BicepValue value) where T : Azure.Provisioning.Primitives.ProvisionableConstruct, new() { throw null; } } public enum BicepValueKind diff --git a/sdk/provisioning/Azure.Provisioning/src/BicepDictionaryOfT.cs b/sdk/provisioning/Azure.Provisioning/src/BicepDictionaryOfT.cs index 50ffc87c725a..254df2a0db51 100644 --- a/sdk/provisioning/Azure.Provisioning/src/BicepDictionaryOfT.cs +++ b/sdk/provisioning/Azure.Provisioning/src/BicepDictionaryOfT.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Collections; using System.Collections.Generic; using System.ComponentModel; @@ -8,6 +9,7 @@ using System.Linq; using Azure.Provisioning.Expressions; using Azure.Provisioning.Primitives; +using Azure.Provisioning.Utilities; namespace Azure.Provisioning; @@ -24,13 +26,10 @@ public class BicepDictionary : private IDictionary> _values; private protected override object? GetLiteralValue() => _values; - // We're empty if unset or no literal values - public override bool IsEmpty => base.IsEmpty || (_kind == BicepValueKind.Literal && _values.Count == 0); - /// /// Creates a new BicepDictionary. /// - public BicepDictionary() : this(self: null, values: null) { } + public BicepDictionary() : this(self: null, values: new Dictionary>()) { } /// /// Creates a new BicepDictionary with literal values. @@ -44,9 +43,18 @@ private protected BicepDictionary(BicepValueReference? self, BicepExpression exp internal BicepDictionary(BicepValueReference? self, IDictionary>? values = null) : base(self) { - _kind = BicepValueKind.Literal; - // Shallow clone their values - _values = values != null ? new Dictionary>(values) : []; + if (values == null) + { + // we consider this as an "uninitialized list" + _kind = BicepValueKind.Unset; + _values = new Dictionary>(); + } + else + { + // in this case, the list is initialized as a literal list + _kind = BicepValueKind.Literal; + _values = new Dictionary>(values); // Shallow clone their values + } } // Move literal elements when assigning values to a dictionary @@ -58,7 +66,15 @@ internal override void Assign(IBicepValue source) { _values = typed._values; } + + // Everything else is handled by the base Assign base.Assign(source); + + // handle self in all the items + foreach (var kv in _values) + { + SetSelfForItem(kv.Value, kv.Key); + } } /// @@ -68,6 +84,22 @@ internal override void Assign(IBicepValue source) public static implicit operator BicepDictionary(ProvisioningVariable reference) => new(new BicepValueReference(reference, "{}"), BicepSyntax.Var(reference.BicepIdentifier)) { _isSecure = reference is ProvisioningParameter p && p.IsSecure }; + private BicepValueReference? GetItemSelf(string key) => + _self is not null + ? new BicepDictionaryValueReference(_self.Construct, _self.PropertyName, _self.BicepPath?.ToArray(), key) + : null; + + private void SetSelfForItem(BicepValue item, string key) + { + var itemSelf = GetItemSelf(key); + item.SetSelf(itemSelf); + } + + private void RemoveSelfForItem(BicepValue item) + { + item.SetSelf(null); + } + /// /// Gets or sets a value in a BicepDictionary. /// @@ -75,16 +107,84 @@ public static implicit operator BicepDictionary(ProvisioningVariable referenc /// The value. public BicepValue this[string key] { - get => _values[key]; - set => _values[key] = value; + get + { + if (_values.TryGetValue(key, out var value)) + { + return value; + } + // The key does not exist; we put a value factory as the literal value of this Bicep value. + // If the value factory is evaluated before the key is added, it will throw a KeyNotFoundException. + // This is valid for generating a reference expression, but will fail if the value is accessed before being added. + return new BicepValue(GetItemSelf(key), () => _values[key].Value); + } + set + { + if (_kind == BicepValueKind.Expression || _isOutput) + { + throw new InvalidOperationException($"Cannot assign to {_self?.PropertyName}, the dictionary is an expression or output only"); + } + if (_kind == BicepValueKind.Unset) + { + _kind = BicepValueKind.Literal; + } + _values[key] = value; + // update the _self pointing the new item + SetSelfForItem(value, key); + } } - public void Add(string key, BicepValue value) => _values.Add(key, value); - public void Add(KeyValuePair> item) => _values.Add(item.Key, item.Value); + public void Add(string key, BicepValue value) + { + if (_kind == BicepValueKind.Expression || _isOutput) + { + throw new InvalidOperationException($"Cannot Add to {_self?.PropertyName}, the dictionary is an expression or output only"); + } + if (_kind == BicepValueKind.Unset) + { + _kind = BicepValueKind.Literal; + } + _values.Add(key, value); + // update the _self pointing the new item + SetSelfForItem(value, key); + } + + public void Add(KeyValuePair> item) => Add(item.Key, item.Value); + + public bool Remove(string key) + { + if (_kind == BicepValueKind.Expression || _isOutput) + { + throw new InvalidOperationException($"Cannot Remove from {_self?.PropertyName}, the dictionary is an expression or output only"); + } + if (_kind == BicepValueKind.Unset) + { + _kind = BicepValueKind.Literal; + } + if (_values.TryGetValue(key, out var removedItem)) + { + // maintain the self reference for the removed item if the item exists + RemoveSelfForItem(removedItem); + } + return _values.Remove(key); + } - // TODO: Decide whether it's important to "unlink" resources on removal - public bool Remove(string key) => _values.Remove(key); - public void Clear() => _values.Clear(); + public void Clear() + { + if (_kind == BicepValueKind.Expression || _isOutput) + { + throw new InvalidOperationException($"Cannot Clear {_self?.PropertyName}, the dictionary is an expression or output only"); + } + if (_kind == BicepValueKind.Unset) + { + _kind = BicepValueKind.Literal; + } + foreach (var kv in _values) + { + RemoveSelfForItem(kv.Value); + } + _values.Clear(); + } public ICollection Keys => _values.Keys; public ICollection> Values => _values.Values; @@ -129,4 +229,14 @@ void ICollection>.CopyTo(KeyValuePair>.Remove(KeyValuePair item) => Remove(item.Key); IEnumerator> IEnumerable>.GetEnumerator() => _values.Select(p => new KeyValuePair(p.Key, p.Value)).GetEnumerator(); + + private protected override BicepExpression CompileLiteralValue() + { + Dictionary compiledValues = []; + foreach (var kv in _values) + { + compiledValues[kv.Key] = kv.Value.Compile(); + } + return BicepSyntax.Object(compiledValues); + } } diff --git a/sdk/provisioning/Azure.Provisioning/src/BicepListOfT.cs b/sdk/provisioning/Azure.Provisioning/src/BicepListOfT.cs index 0a177a379276..d7c60c39c102 100644 --- a/sdk/provisioning/Azure.Provisioning/src/BicepListOfT.cs +++ b/sdk/provisioning/Azure.Provisioning/src/BicepListOfT.cs @@ -5,8 +5,10 @@ using System.Collections; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using Azure.Provisioning.Expressions; using Azure.Provisioning.Primitives; +using Azure.Provisioning.Utilities; namespace Azure.Provisioning; @@ -23,14 +25,10 @@ public class BicepList : private IList> _values; private protected override object? GetLiteralValue() => _values; - // We're empty if unset or no literal values - public override bool IsEmpty => - base.IsEmpty || (_kind == BicepValueKind.Literal && _values.Count == 0); - /// /// Creates a new BicepList. /// - public BicepList() : this(self: null, values: null) { } + public BicepList() : this(self: null, values: []) { } /// /// Creates a new BicepList with literal values. @@ -42,9 +40,18 @@ public BicepList(IList>? values) : this(self: null, values) { } internal BicepList(BicepValueReference? self, IList>? values = null) : base(self) { - _kind = BicepValueKind.Literal; - // Shallow clone their list - _values = values != null ? [.. values] : []; + if (values == null) + { + // we consider this as an "uninitialized list" + _kind = BicepValueKind.Unset; + _values = []; + } + else + { + // in this case, the list is initialized as a literal list + _kind = BicepValueKind.Literal; + _values = [.. values]; // Shallow clone their list + } } // Move literal elements when assigning values to a list @@ -59,6 +66,12 @@ internal override void Assign(IBicepValue source) // Everything else is handled by the base Assign base.Assign(source); + + // handle self in all the items + for (int i = 0; i < _values.Count; i++) + { + SetSelfForItem(_values[i], i); + } } /// @@ -91,7 +104,18 @@ public BicepValue this[int index] } else { - return _values[index]; + if (index < 0) + { + throw new ArgumentOutOfRangeException(nameof(index), "Index must be non-negative."); + } + // if we are referencing an actual value + if (index < _values.Count) + { + return _values[index]; + } + // the index is out of range, we put a value factory as the literal value of this bicep value + // this will throw an exception when evaluated or compiled, but is still usable for building a reference expression + return new BicepValue(GetItemSelf(index), () => _values[index].Value); } } set @@ -100,17 +124,120 @@ public BicepValue this[int index] { throw new InvalidOperationException($"Cannot assign to {_self?.PropertyName}"); } + if (_kind == BicepValueKind.Unset) + { + _kind = BicepValueKind.Literal; + } _values[index] = value; + // update the _self pointing the new item + SetSelfForItem(value, index); } } - public void Insert(int index, BicepValue item) => _values.Insert(index, item); - public void Add(BicepValue item) => _values.Add(item); + private BicepValueReference? GetItemSelf(int index) => + _self is not null + ? new BicepListValueReference(_self.Construct, _self.PropertyName, _self.BicepPath?.ToArray(), index) + : null; + + private void SetSelfForItem(BicepValue item, int index) + { + var itemSelf = GetItemSelf(index); + item.SetSelf(itemSelf); + } + + private void RemoveSelfForItem(BicepValue item) + { + item.SetSelf(null); + } - // TODO: Decide whether it's important to "unlink" resources on removal - public void RemoveAt(int index) => _values.RemoveAt(index); - public void Clear() => _values.Clear(); - public bool Remove(BicepValue item) => _values.Remove(item); + public void Insert(int index, BicepValue item) + { + if (_kind == BicepValueKind.Expression || _isOutput) + { + throw new InvalidOperationException($"Cannot Insert to {_self?.PropertyName}, the list is an expression or output only"); + } + if (_kind == BicepValueKind.Unset) + { + _kind = BicepValueKind.Literal; + } + _values.Insert(index, item); + // update the _self for the inserted item and all items after it + for (int i = index; i < _values.Count; i++) + { + SetSelfForItem(_values[i], i); + } + } + + public void Add(BicepValue item) + { + if (_kind == BicepValueKind.Expression || _isOutput) + { + throw new InvalidOperationException($"Cannot Add to {_self?.PropertyName}, the list is an expression or output only"); + } + if (_kind == BicepValueKind.Unset) + { + _kind = BicepValueKind.Literal; + } + _values.Add(item); + // update the _self pointing the new item + SetSelfForItem(item, _values.Count - 1); + } + + public void RemoveAt(int index) + { + if (_kind == BicepValueKind.Expression || _isOutput) + { + throw new InvalidOperationException($"Cannot Remove from {_self?.PropertyName}, the list is an expression or output only"); + } + if (_kind == BicepValueKind.Unset) + { + _kind = BicepValueKind.Literal; + } + var removed = _values[index]; + _values.RemoveAt(index); + // maintain the self reference for the removed item and remaining items + RemoveSelfForItem(removed); + for (int i = index; i < _values.Count; i++) + { + SetSelfForItem(_values[i], i); + } + } + + public void Clear() + { + if (_kind == BicepValueKind.Expression || _isOutput) + { + throw new InvalidOperationException($"Cannot Clear {_self?.PropertyName}, the list is an expression or output only"); + } + if (_kind == BicepValueKind.Unset) + { + _kind = BicepValueKind.Literal; + } + for (int i = 0; i < _values.Count; i++) + { + RemoveSelfForItem(_values[i]); + } + _values.Clear(); + } + + public bool Remove(BicepValue item) + { + if (_kind == BicepValueKind.Expression || _isOutput) + { + throw new InvalidOperationException($"Cannot Remove from {_self?.PropertyName}, the list is an expression or output only"); + } + if (_kind == BicepValueKind.Unset) + { + _kind = BicepValueKind.Literal; + } + int index = _values.IndexOf(item); + if (index >= 0) + { + RemoveAt(index); + return true; + } + return false; + } public int Count => _values.Count; public bool IsReadOnly => _values.IsReadOnly; @@ -135,4 +262,9 @@ public BicepValue this[int index] public static BicepList FromExpression(Func referenceFactory, BicepExpression expression) => new(expression) { _referenceFactory = referenceFactory }; private Func? _referenceFactory = null; + + private protected override BicepExpression CompileLiteralValue() + { + return BicepSyntax.Array(_values.Select(v => v.Compile()).ToArray()); + } } diff --git a/sdk/provisioning/Azure.Provisioning/src/BicepValue.cs b/sdk/provisioning/Azure.Provisioning/src/BicepValue.cs index 3f449b44fd26..b38d17b2ffef 100644 --- a/sdk/provisioning/Azure.Provisioning/src/BicepValue.cs +++ b/sdk/provisioning/Azure.Provisioning/src/BicepValue.cs @@ -90,7 +90,33 @@ private protected BicepValue(BicepValueReference? self, BicepExpression expressi public override string ToString() => Compile().ToString(); /// - public BicepExpression Compile() => BicepTypeMapping.ToBicep(this, Format); + public BicepExpression Compile() + { + if (_kind == BicepValueKind.Expression) + { + return _expression!; + } + if (_source is not null) + { + return _source.GetReference(); + } + if (_kind == BicepValueKind.Literal) + { + return CompileLiteralValue(); + } + if (_self is not null) + { + return _self.GetReference(); + } + if (_kind is BicepValueKind.Unset) + { + return BicepSyntax.Null(); + } + + throw new InvalidOperationException($"Cannot convert {this} to a Bicep expression."); + } + + private protected abstract BicepExpression CompileLiteralValue(); /// void IBicepValue.Assign(IBicepValue source) => Assign(source); diff --git a/sdk/provisioning/Azure.Provisioning/src/BicepValueExtensions.cs b/sdk/provisioning/Azure.Provisioning/src/BicepValueExtensions.cs index 8b9d943091f8..46887ad727f1 100644 --- a/sdk/provisioning/Azure.Provisioning/src/BicepValueExtensions.cs +++ b/sdk/provisioning/Azure.Provisioning/src/BicepValueExtensions.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; +using Azure.Provisioning.Expressions; using Azure.Provisioning.Primitives; namespace Azure.Provisioning; @@ -34,4 +34,28 @@ public static T Unwrap(this BicepValue value) } // TODO: Add more common casts + + /// + /// Convert a IBicepValue into a BicepExpression by its reference to represent its hierarchy. + /// + /// + /// + public static BicepExpression ToBicepExpression(this IBicepValue bicepValue) + { + // if self is set, we could build an expression as a reference of this member + if (bicepValue.Self is not null) + { + return bicepValue.Self.GetReference(); + } + // if self is not set, but the value of this is an expression, we return that expression + else if (bicepValue.Kind == BicepValueKind.Expression) + { + return bicepValue.Expression ?? BicepSyntax.Null(); + } + // otherwise, we return whatever this compiles into + else + { + return bicepValue.Compile(); + } + } } diff --git a/sdk/provisioning/Azure.Provisioning/src/BicepValueOfT.cs b/sdk/provisioning/Azure.Provisioning/src/BicepValueOfT.cs index c5ade9494731..dd4d82467826 100644 --- a/sdk/provisioning/Azure.Provisioning/src/BicepValueOfT.cs +++ b/sdk/provisioning/Azure.Provisioning/src/BicepValueOfT.cs @@ -3,6 +3,8 @@ using System; using System.ComponentModel; +using System.Net; +using Azure.Core; using Azure.Provisioning.Expressions; using Azure.Provisioning.Primitives; @@ -17,11 +19,25 @@ namespace Azure.Provisioning; /// The type of the value. public class BicepValue : BicepValue { + private T? _value; + private Func? _valueFactory; /// /// Gets or sets the literal value. You can also rely on implicit /// conversions most of the time. /// - public T? Value { get; private protected set; } + public T? Value + { + get => _valueFactory is not null ? _valueFactory() : _value; + private set + { + if (_valueFactory is not null) + { + throw new InvalidOperationException($"Cannot assign value for {_self?.GetReference(false)} because its value is invalid"); + } + _value = value; + } + } + private protected override object? GetLiteralValue() => Value; // Get the closest primitive to T @@ -40,9 +56,42 @@ public BicepValue(T literal) : this(self: null, literal) { } /// An expression that evaluates to the value. public BicepValue(BicepExpression expression) : this(self: null, expression) { } - internal BicepValue(BicepValueReference? self) : base(self) { } - private protected BicepValue(BicepValueReference? self, T literal) : base(self, (object)literal!) { Value = literal; } - private protected BicepValue(BicepValueReference? self, BicepExpression expression) : base(self, expression) { } + /// + /// Initialize a new instance without a literal value or expression (unset state). + /// + /// + internal BicepValue(BicepValueReference? self) : base(self) + { + } + + /// + /// Initialize a new instance of a literal value, but the literal value will be lazily evaluated by the value factory. + /// + /// + /// + internal BicepValue(BicepValueReference? self, Func valueFactory) : base(self) + { + _kind = BicepValueKind.Literal; + _valueFactory = valueFactory; + } + + /// + /// Initialize a new instance from a literal value. + /// + /// + /// + private BicepValue(BicepValueReference? self, T literal) : base(self, literal!) + { + _value = literal; + } + + /// + /// Initialize a new instance from a bicep expression. + /// + /// + /// + private BicepValue(BicepValueReference? self, BicepExpression expression) : base(self, expression) + { } /// /// Clears a previously assigned literal or expression value. @@ -50,7 +99,8 @@ private protected BicepValue(BicepValueReference? self, BicepExpression expressi public void ClearValue() { _kind = BicepValueKind.Unset; - Value = default; + _valueFactory = null; + _value = default; _expression = null; _source = null; } @@ -113,4 +163,29 @@ public static implicit operator BicepValue(BicepValue value) => BicepValueKind.Literal => new(value._self, BicepTypeMapping.ToLiteralString(value.Value!, value.Format)), _ => throw new InvalidOperationException($"Unknown {nameof(BicepValueKind)}!") }; + + private protected override BicepExpression CompileLiteralValue() => Value switch + { + null => BicepSyntax.Null(), + IBicepValue v => v.Compile(), + bool b => BicepSyntax.Value(b), + int i => BicepSyntax.Value(i), + long l => BicepSyntax.Value(l), + float f => BicepSyntax.Value(f), + double d => BicepSyntax.Value(d), + string s => BicepSyntax.Value(s), + Uri u => BicepSyntax.Value(BicepTypeMapping.ToLiteralString(u, Format)), + DateTimeOffset d => BicepSyntax.Value(BicepTypeMapping.ToLiteralString(d, Format)), + TimeSpan t => BicepSyntax.Value(BicepTypeMapping.ToLiteralString(t, Format)), + Guid g => BicepSyntax.Value(BicepTypeMapping.ToLiteralString(g, Format)), + IPAddress a => BicepSyntax.Value(BicepTypeMapping.ToLiteralString(a, Format)), + ETag e => BicepSyntax.Value(BicepTypeMapping.ToLiteralString(e, Format)), + ResourceIdentifier i => BicepSyntax.Value(BicepTypeMapping.ToLiteralString(i, Format)), + AzureLocation azureLocation => BicepSyntax.Value(BicepTypeMapping.ToLiteralString(azureLocation, Format)), + ResourceType rt => BicepSyntax.Value(BicepTypeMapping.ToLiteralString(rt, Format)), + Enum e => BicepSyntax.Value(BicepTypeMapping.ToLiteralString(e, Format)), + // Other extensible enums like AzureLocation (AzureLocation has been handled above) + ValueType ee => BicepSyntax.Value(BicepTypeMapping.ToLiteralString(ee, Format)), + _ => throw new InvalidOperationException($"Cannot convert {Value} to a Bicep expression.") + }; } diff --git a/sdk/provisioning/Azure.Provisioning/src/Expressions/BicepSyntax.cs b/sdk/provisioning/Azure.Provisioning/src/Expressions/BicepSyntax.cs index 30fb806f2d80..f3f3be39a6a9 100644 --- a/sdk/provisioning/Azure.Provisioning/src/Expressions/BicepSyntax.cs +++ b/sdk/provisioning/Azure.Provisioning/src/Expressions/BicepSyntax.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.Linq; @@ -17,6 +18,26 @@ internal static class BicepSyntax public static NullLiteralExpression Null() => new(); public static BoolLiteralExpression Value(bool value) => new(value); public static IntLiteralExpression Value(int value) => new(value); + public static BicepExpression Value(long value) + { + // see if the value falls into the int range + if (value >= int.MinValue && value <= int.MaxValue) + { + return BicepSyntax.Value((int)value); + } + // otherwise we use the workaround from https://github.com/Azure/bicep/issues/1386#issuecomment-818077233 + return BicepFunction.ParseJson(BicepSyntax.Value(value.ToString())).Compile(); + } + public static BicepExpression Value(double value) + { + // see if the value is a whole number + if (value >= int.MinValue && value <= int.MaxValue && value == Math.Floor(value)) + { + return BicepSyntax.Value((int)value); + } + // otherwise we use the workaround from https://github.com/Azure/bicep/issues/1386#issuecomment-818077233 + return BicepFunction.ParseJson(BicepSyntax.Value(value.ToString())).Compile(); + } public static StringLiteralExpression Value(string value) => new(value); public static ArrayExpression Array(params BicepExpression[] values) => new(values); public static ObjectExpression Object(IDictionary properties) => new(properties.Keys.Select(k => new PropertyExpression(k, properties[k])).ToArray()); diff --git a/sdk/provisioning/Azure.Provisioning/src/Expressions/BicepTypeMapping.cs b/sdk/provisioning/Azure.Provisioning/src/Expressions/BicepTypeMapping.cs index edc639d2ba61..f41ce189be20 100644 --- a/sdk/provisioning/Azure.Provisioning/src/Expressions/BicepTypeMapping.cs +++ b/sdk/provisioning/Azure.Provisioning/src/Expressions/BicepTypeMapping.cs @@ -2,15 +2,11 @@ // Licensed under the MIT License. using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; using System.Net; using System.Reflection; using System.Runtime.Serialization; using System.Xml; using Azure.Core; -using Azure.Provisioning.Primitives; namespace Azure.Provisioning.Expressions; @@ -80,97 +76,6 @@ public static string ToLiteralString(object value, string? format) => _ => throw new InvalidOperationException($"Cannot convert {value} to a literal Bicep string.") }; - /// - /// Convert a .NET object into a Bicep expression. - /// - /// The .NET value. - /// Optional format. - /// The corresponding Bicep expression. - /// - /// Thrown when we cannot convert a value to a Bicep expression. - /// - public static BicepExpression ToBicep(object? value, string? format) - { - return value switch - { - null => BicepSyntax.Null(), - bool b => BicepSyntax.Value(b), - int i => BicepSyntax.Value(i), - long l => FromLong(l), - // Note: bicep does not offically support floating numbers - // therefore for floating numbers we are taking a workaround from - // https://github.com/Azure/bicep/issues/1386#issuecomment-818077233 - float f => FromDouble(f), - double d => FromDouble(d), - string s => BicepSyntax.Value(s), - Uri u => BicepSyntax.Value(ToLiteralString(u, format)), - DateTimeOffset d => BicepSyntax.Value(ToLiteralString(d, format)), - TimeSpan t => BicepSyntax.Value(ToLiteralString(t, format)), - Guid g => BicepSyntax.Value(ToLiteralString(g, format)), - IPAddress a => BicepSyntax.Value(ToLiteralString(a, format)), - ETag e => BicepSyntax.Value(ToLiteralString(e, format)), - ResourceIdentifier i => BicepSyntax.Value(ToLiteralString(i, format)), - AzureLocation azureLocation => BicepSyntax.Value(ToLiteralString(azureLocation, format)), - ResourceType rt => BicepSyntax.Value(ToLiteralString(rt, format)), - Enum e => BicepSyntax.Value(ToLiteralString(e, format)), - // we call this method on IBicepValue to convert this into an expression instead of statements - ProvisionableConstruct c => ((IBicepValue)c).Compile(), - IDictionary d => - d is IBicepValue b && b.Kind == BicepValueKind.Expression ? b.Expression! : ToObject(d), - IEnumerable seq => - seq is IBicepValue b && b.Kind == BicepValueKind.Expression ? b.Expression! : ToArray(seq.OfType()), - // Other extensible enums like AzureLocation (AzureLocation has been handled above) - ValueType ee => BicepSyntax.Value(ToLiteralString(ee, format)), - // Unwrap BicepValue after collections so it doesn't loop forever - IBicepValue v when (v.Kind == BicepValueKind.Expression) => v.Expression!, - IBicepValue v when (v.Source is not null) => v.Source.GetReference(), - IBicepValue v when (v.Kind == BicepValueKind.Literal) => ToBicep(v.LiteralValue, format), - IBicepValue v when (v.Self is not null) => v.Self.GetReference(), - IBicepValue v when (v.Kind == BicepValueKind.Unset) => BicepSyntax.Null(), - _ => throw new InvalidOperationException($"Cannot convert {value} to a Bicep expression.") - }; - - BicepExpression FromLong(long l) - { - // see if the value falls into the int range - if (l >= int.MinValue && l <= int.MaxValue) - { - return BicepSyntax.Value((int)l); - } - // otherwise we use the workaround from https://github.com/Azure/bicep/issues/1386#issuecomment-818077233 - return BicepFunction.ParseJson(BicepSyntax.Value(l.ToString())).Compile(); - } - - BicepExpression FromDouble(double d) - { - // see if the value is a whole number - if (d >= int.MinValue && d <= int.MaxValue && d == Math.Floor(d)) - { - return BicepSyntax.Value((int)d); - } - // otherwise we use the workaround from https://github.com/Azure/bicep/issues/1386#issuecomment-818077233 - return BicepFunction.ParseJson(BicepSyntax.Value(d.ToString())).Compile(); - } - - ArrayExpression ToArray(IEnumerable seq) => - BicepSyntax.Array([.. seq.Select(v => ToBicep(v, v is BicepValue b ? b.Format : null))]); - - ObjectExpression ToObject(IDictionary dict) - { - Dictionary values = []; - foreach (KeyValuePair pair in dict) - { - string? format = null; - if (pair.Value is BicepValue v) - { - format = v.Format; - } - values[pair.Key] = ToBicep(pair.Value, format); - } - return BicepSyntax.Object(values); - } - } - /// /// Get the value of an enum. This is either the name of the enum value or /// optionally overridden by a DataMember attribute when the wire value diff --git a/sdk/provisioning/Azure.Provisioning/src/Primitives/BicepValueReference.cs b/sdk/provisioning/Azure.Provisioning/src/Primitives/BicepValueReference.cs index fd31651860b3..f08bea81d9e8 100644 --- a/sdk/provisioning/Azure.Provisioning/src/Primitives/BicepValueReference.cs +++ b/sdk/provisioning/Azure.Provisioning/src/Primitives/BicepValueReference.cs @@ -13,7 +13,7 @@ public class BicepValueReference(ProvisionableConstruct construct, string proper public string PropertyName { get; } = propertyName; public IReadOnlyList? BicepPath { get; } = path; - internal BicepExpression GetReference(bool throwIfNoRoot = true) + internal virtual BicepExpression GetReference(bool throwIfNoRoot = true) { // Get the root BicepExpression? target = ((IBicepValue)Construct).Self?.GetReference(); @@ -25,7 +25,7 @@ internal BicepExpression GetReference(bool throwIfNoRoot = true) } else if (throwIfNoRoot) { - throw new NotImplementedException("Cannot reference a construct without a name."); + throw new InvalidOperationException("Cannot reference a construct without a name."); } else { @@ -48,3 +48,24 @@ internal BicepExpression GetReference(bool throwIfNoRoot = true) public override string ToString() => GetReference(throwIfNoRoot: false).ToString(); } + +internal class BicepListValueReference(ProvisionableConstruct construct, string propertyName, string[]? path, int index) + : BicepValueReference(construct, propertyName, path) +{ + public int Index { get; } = index; + + internal override BicepExpression GetReference(bool throwIfNoRoot = true) + { + return base.GetReference(throwIfNoRoot).Index(new IntLiteralExpression(Index)); + } +} + +internal class BicepDictionaryValueReference(ProvisionableConstruct construct, string propertyName, string[]? path, string key) + : BicepValueReference(construct, propertyName, path) +{ + public string Key { get; } = key; + internal override BicepExpression GetReference(bool throwIfNoRoot = true) + { + return base.GetReference(throwIfNoRoot).Index(Key); + } +} diff --git a/sdk/provisioning/Azure.Provisioning/src/Primitives/ProvisionableConstruct.cs b/sdk/provisioning/Azure.Provisioning/src/Primitives/ProvisionableConstruct.cs index 67d9ec5d166d..6dc376e22f1f 100644 --- a/sdk/provisioning/Azure.Provisioning/src/Primitives/ProvisionableConstruct.cs +++ b/sdk/provisioning/Azure.Provisioning/src/Primitives/ProvisionableConstruct.cs @@ -309,7 +309,7 @@ protected BicepList DefineListProperty( bool isRequired = false) { BicepList values = - new(new BicepValueReference(this, propertyName, bicepPath)) + new(new BicepValueReference(this, propertyName, bicepPath), values: null) // we call this ctor to initialize an "uninitialized" list { _isOutput = isOutput, _isRequired = isRequired @@ -325,7 +325,7 @@ protected BicepDictionary DefineDictionaryProperty( bool isRequired = false) { BicepDictionary values = - new(new BicepValueReference(this, propertyName, bicepPath)) + new(new BicepValueReference(this, propertyName, bicepPath), values: null) // we call this ctor to initialize an "uninitialized" dictionary { _isOutput = isOutput, _isRequired = isRequired diff --git a/sdk/provisioning/Azure.Provisioning/src/Utilities/BicepValueHelpers.cs b/sdk/provisioning/Azure.Provisioning/src/Utilities/BicepValueHelpers.cs new file mode 100644 index 000000000000..f45a2513a429 --- /dev/null +++ b/sdk/provisioning/Azure.Provisioning/src/Utilities/BicepValueHelpers.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Provisioning.Primitives; + +namespace Azure.Provisioning.Utilities +{ + internal static class BicepValueHelpers + { + internal static void SetSelf(this IBicepValue value, BicepValueReference? self) + { + IBicepValue? bicepValue = value; + while (bicepValue is IBicepValue) + { + bicepValue.Self = self; + if (bicepValue is ProvisionableConstruct) + { + break; // Stop traversal at ProvisionableConstruct to avoid repeatedly processing the same instance (which would cause an infinite loop or redundant work), since its LiteralValue refers back to itself. + } + bicepValue = bicepValue.LiteralValue as IBicepValue; + } + } + } +} diff --git a/sdk/provisioning/Azure.Provisioning/tests/Azure.Provisioning.Tests.csproj b/sdk/provisioning/Azure.Provisioning/tests/Azure.Provisioning.Tests.csproj index a486bd292a10..f1e3c5cb15b1 100644 --- a/sdk/provisioning/Azure.Provisioning/tests/Azure.Provisioning.Tests.csproj +++ b/sdk/provisioning/Azure.Provisioning/tests/Azure.Provisioning.Tests.csproj @@ -6,6 +6,7 @@ + diff --git a/sdk/provisioning/Azure.Provisioning/tests/BicepValues/BicepValueTests.cs b/sdk/provisioning/Azure.Provisioning/tests/BicepValues/BicepValueTests.cs index 7a61783d6d5e..458ad12e5594 100644 --- a/sdk/provisioning/Azure.Provisioning/tests/BicepValues/BicepValueTests.cs +++ b/sdk/provisioning/Azure.Provisioning/tests/BicepValues/BicepValueTests.cs @@ -66,7 +66,7 @@ public async Task ValidateTimeSpanPropertyWithFormat() """ @description('The location for the resource(s) to be deployed.') param location string = resourceGroup().location - + resource script 'Microsoft.Resources/deploymentScripts@2023-08-01' = { name: take('script${uniqueString(resourceGroup().id)}', 24) location: location diff --git a/sdk/provisioning/Azure.Provisioning/tests/BicepValues/BicepValueToExpressionTests.cs b/sdk/provisioning/Azure.Provisioning/tests/BicepValues/BicepValueToExpressionTests.cs new file mode 100644 index 000000000000..5d3a88d4aeb2 --- /dev/null +++ b/sdk/provisioning/Azure.Provisioning/tests/BicepValues/BicepValueToExpressionTests.cs @@ -0,0 +1,991 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Azure.Provisioning.Expressions; +using Azure.Provisioning.Primitives; +using NUnit.Framework; + +namespace Azure.Provisioning.Tests.BicepValues +{ + public class BicepValueToExpressionTests + { + [Test] + public void ValidateLiteralValue() + { + var stringLiteral = new BicepValue("literal"); + TestHelpers.AssertExpression("'literal'", stringLiteral); + TestHelpers.AssertExpression("'literal'", stringLiteral.ToBicepExpression()); + + var intLiteral = new BicepValue(42); + TestHelpers.AssertExpression("42", intLiteral); + TestHelpers.AssertExpression("42", intLiteral.ToBicepExpression()); + + var boolLiteral = new BicepValue(true); + TestHelpers.AssertExpression("true", boolLiteral); + TestHelpers.AssertExpression("true", boolLiteral.ToBicepExpression()); + + var doubleLiteral = new BicepValue(3.14); + TestHelpers.AssertExpression("json('3.14')", doubleLiteral); + TestHelpers.AssertExpression("json('3.14')", doubleLiteral.ToBicepExpression()); + } + + [Test] + public void ValidateExpressionValue() + { + var expression = BicepFunction.GetUniqueString("a"); + TestHelpers.AssertExpression("uniqueString('a')", expression); + TestHelpers.AssertExpression("uniqueString('a')", expression.ToBicepExpression()); + } + + [Test] + public void ValidateSimpleProperty() + { + var resource = new TestResource("test"); + resource.WithValue = "foo"; + var withValue = resource.WithValue; + + TestHelpers.AssertExpression("'foo'", withValue); + TestHelpers.AssertExpression("test.withValue", withValue.ToBicepExpression()); + + var withoutValue = resource.WithoutValue; + TestHelpers.AssertExpression("test.withoutValue", withoutValue); + TestHelpers.AssertExpression("test.withoutValue", withoutValue.ToBicepExpression()); + } + + [Test] + public void ValidateNestedProperty() + { + var resource = new TestResource("test") + { + Properties = new TestProperties() + { + WithValue = "nestedValue" + } + }; + + var nested = resource.Properties.WithValue; + + TestHelpers.AssertExpression("'nestedValue'", nested); + TestHelpers.AssertExpression("test.properties.withValue", nested.ToBicepExpression()); + + var nestedWithoutValue = resource.Properties.WithoutValue; + TestHelpers.AssertExpression("test.properties.withoutValue", nestedWithoutValue); + TestHelpers.AssertExpression("test.properties.withoutValue", nestedWithoutValue.ToBicepExpression()); + } + + [Test] + public void ValidateOutputNestedProperty() + { + var resource = new TestResource("test"); + var outputNested = resource.OutputModel.Id; + + TestHelpers.AssertExpression("test.outputModel.id", outputNested); + TestHelpers.AssertExpression("test.outputModel.id", outputNested.ToBicepExpression()); + } + + [Test] + public void ValidateListProperty_Undefined() + { + var resource = new TestResource("test"); + + TestHelpers.AssertExpression("test.list", resource.List); // undefined list should only print the expression + TestHelpers.AssertExpression("test.list", resource.List.ToBicepExpression()); + } + + [Test] + public void ValidateListProperty_Empty_ExplicitCallClear() + { + var resource = new TestResource("test"); + resource.List.Clear(); // explicitly clear to make it empty + + TestHelpers.AssertExpression("[]", resource.List); + TestHelpers.AssertExpression("test.list", resource.List.ToBicepExpression()); + } + + [Test] + public void ValidateListProperty_Empty_AssignNewList() + { + var resource = new TestResource("test"); + resource.List = new BicepList(); // assign a new empty list + + TestHelpers.AssertExpression("[]", resource.List); + TestHelpers.AssertExpression("test.list", resource.List.ToBicepExpression()); + } + + [Test] + public void ValidateListProperty_Empty_AddAndRemoveAll() + { + var resource = new TestResource("test") + { + List = + { + "item1" + } + }; + resource.List.RemoveAt(0); // all the items are removed, now the list is empty + TestHelpers.AssertExpression("[]", resource.List); + TestHelpers.AssertExpression("test.list", resource.List.ToBicepExpression()); + } + + [Test] + public void ValidateListProperty_WithValues() + { + var resource = new TestResource("test"); + resource.List.Add("item1"); + resource.List.Add("item2"); + TestHelpers.AssertExpression(""" + [ + 'item1' + 'item2' + ] + """, resource.List); + TestHelpers.AssertExpression("test.list", resource.List.ToBicepExpression()); + } + + [Test] + public void ValidateListProperty_Indexer() + { + var resource = new TestResource("test"); + resource.List.Add("item1"); + var validIndexer = resource.List[0]; + TestHelpers.AssertExpression("'item1'", validIndexer); + TestHelpers.AssertExpression("test.list[0]", validIndexer.ToBicepExpression()); + + var invalidIndexer = resource.List[1]; // this is an out-of-range index + Assert.Throws(() => invalidIndexer.ToString()); + TestHelpers.AssertExpression("test.list[1]", invalidIndexer.ToBicepExpression()); + } + + [Test] + public void ValidateListProperty_IndexerWithAssign() + { + var resource = new TestResource("test"); + resource.List.Add("item1"); + var validIndexer = resource.List[0]; + // call Assign to change the value of this + validIndexer.Assign("updatedItem1"); + TestHelpers.AssertExpression("'updatedItem1'", validIndexer); + TestHelpers.AssertExpression("test.list[0]", validIndexer.ToBicepExpression()); + + var invalidIndexer = resource.List[1]; // this is an out-of-range index + Assert.Throws(() => invalidIndexer.ToString()); + Assert.Throws(() => invalidIndexer.Assign("newItem")); // since this out of range, we can't assign a value to it + TestHelpers.AssertExpression("test.list[1]", invalidIndexer.ToBicepExpression()); + } + + [Test] + public void ValidateModelListProperty_Undefined() + { + var resource = new TestResource("test"); + // untouched list will not appear in the final result's resource + TestHelpers.AssertExpression("test.models", resource.Models); + TestHelpers.AssertExpression("test.models", resource.Models.ToBicepExpression()); + } + + [Test] + public void ValidateModelListProperty_Empty_ExplicitCallClear() + { + var resource = new TestResource("test"); + resource.Models.Clear(); // explicitly clear to make it empty + TestHelpers.AssertExpression("[]", resource.Models); + TestHelpers.AssertExpression("test.models", resource.Models.ToBicepExpression()); + } + + [Test] + public void ValidateModelListProperty_Empty_AssignNewList() + { + var resource = new TestResource("test"); + resource.Models = new BicepList(); // assign a new empty list + TestHelpers.AssertExpression("[]", resource.Models); + TestHelpers.AssertExpression("test.models", resource.Models.ToBicepExpression()); + } + + [Test] + public void ValidateModelListProperty_Empty_AddAndRemoveAll() + { + var resource = new TestResource("test") + { + Models = + { + new TestModel() { Name = "model1" } + } + }; + resource.Models.RemoveAt(0); // all the items are removed, now the list is empty + TestHelpers.AssertExpression("[]", resource.Models); + TestHelpers.AssertExpression("test.models", resource.Models.ToBicepExpression()); + } + + [Test] + public void ValidateModelListProperty_WithValues() + { + var resource = new TestResource("test"); + resource.Models.Add(new TestModel() { Name = "model1" }); + resource.Models.Add(new TestModel() { Name = "model2" }); + TestHelpers.AssertExpression(""" + [ + { + name: 'model1' + } + { + name: 'model2' + } + ] + """, resource.Models); + TestHelpers.AssertExpression("test.models", resource.Models.ToBicepExpression()); + } + + [Test] + public void ValidateModelListProperty_Indexer() + { + var resource = new TestResource("test"); + resource.Models.Add(new TestModel() { Name = "model1" }); + var validIndexer = resource.Models[0]; + TestHelpers.AssertExpression(""" + { + name: 'model1' + } + """, validIndexer); + TestModel? model = validIndexer.Value; + Assert.IsNotNull(model); + TestHelpers.AssertExpression("'model1'", model!.Name); + TestHelpers.AssertExpression("test.models[0]", validIndexer.ToBicepExpression()); + + var name = model.Name; + TestHelpers.AssertExpression("'model1'", name); + TestHelpers.AssertExpression("test.models[0].name", name.ToBicepExpression()); + + // change the name + model!.Name = "updatedModel1"; + TestHelpers.AssertExpression(""" + { + name: 'updatedModel1' + } + """, validIndexer); + + var invalidIndexer = resource.Models[1]; // out-of-range + Assert.Throws(() => _ = invalidIndexer.Value); + TestHelpers.AssertExpression("test.models[1]", invalidIndexer.ToBicepExpression()); + } + + [Test] + public void ValidateModelListProperty_ItemModelProperties() + { + var resource = new TestResource("test"); + resource.Models.Add(new TestModel() { Name = "model1" }); + var validIndexer = resource.Models[0]; + + var item = validIndexer.Value!; + Assert.IsNotNull(item); + var name = item.Name; + + TestHelpers.AssertExpression("'model1'", name); + TestHelpers.AssertExpression("test.models[0].name", name.ToBicepExpression()); + } + + [Test] + public void ValidateModelListProperty_ItemModelProperties_FromModelInstance() + { + var resource = new TestResource("test"); + var modelInstance = new TestModel() { Name = "model1" }; + resource.Models.Add(modelInstance); + + var name = modelInstance.Name; + TestHelpers.AssertExpression("'model1'", name); + TestHelpers.AssertExpression("test.models[0].name", name.ToBicepExpression()); + + // validates the expression would change if the list is updated + resource.Models.Insert(0, new TestModel() { Name = "model0" }); + // now the `modelInstance` is at index 1 + TestHelpers.AssertExpression("test.models[1].name", name.ToBicepExpression()); + } + + [Test] + public void ValidateOutputListProperty() + { + var resource = new TestResource("test"); + TestHelpers.AssertExpression("test.outputList", resource.OutputList); + TestHelpers.AssertExpression("test.outputList", resource.OutputList.ToBicepExpression()); + + // trying to modify this list should lead to exceptions + Assert.Throws(() => resource.OutputList.Add("item")); + Assert.Throws(() => resource.OutputList.Clear()); + } + + [Test] + public void ValidateOutputListProperty_Indexer() + { + var resource = new TestResource("test"); + // add value to an output list will throw + Assert.Throws(() => resource.OutputList.Add("outputItem1")); + // call the setter of indexer will throw + Assert.Throws(() => resource.OutputList[0] = "outputItem1"); + + var validIndexer = resource.OutputList[0]; + Assert.Throws(() => validIndexer.ToString()); + TestHelpers.AssertExpression("test.outputList[0]", validIndexer.ToBicepExpression()); + } + [Test] + public void ValidateNestedListProperty_Undefined() + { + var resource = new TestResource("test") + { + Properties = new TestProperties() + }; + var nestedList = resource.Properties.List; + TestHelpers.AssertExpression("test.properties.list", nestedList); + TestHelpers.AssertExpression("test.properties.list", nestedList.ToBicepExpression()); + } + + [Test] + public void ValidateNestedListProperty_Empty_ExplicitCallClear() + { + var resource = new TestResource("test") + { + Properties = new TestProperties() + }; + resource.Properties.List.Clear(); // explicitly clear to make it empty + TestHelpers.AssertExpression("[]", resource.Properties.List); + TestHelpers.AssertExpression("test.properties.list", resource.Properties.List.ToBicepExpression()); + } + + [Test] + public void ValidateNestedListProperty_Empty_AssignNewList() + { + var resource = new TestResource("test") + { + Properties = new TestProperties() + }; + resource.Properties.List = new BicepList(); // assign a new empty list + TestHelpers.AssertExpression("[]", resource.Properties.List); + TestHelpers.AssertExpression("test.properties.list", resource.Properties.List.ToBicepExpression()); + } + + [Test] + public void ValidateNestedListProperty_Empty_AddAndRemoveAll() + { + var resource = new TestResource("test") + { + Properties = new TestProperties() + { + List = + { + "nestedItem1" + } + } + }; + resource.Properties.List.RemoveAt(0); // all the items are removed, now the list is empty + TestHelpers.AssertExpression("[]", resource.Properties.List); + TestHelpers.AssertExpression("test.properties.list", resource.Properties.List.ToBicepExpression()); + } + + [Test] + public void ValidateNestedListProperty_Empty_AddAndClear() + { + var resource = new TestResource("test") + { + Properties = new TestProperties() + }; + resource.Properties.List.Add("nestedItem1"); + resource.Properties.List.Clear(); // all the items are removed, now the list is empty + TestHelpers.AssertExpression("[]", resource.Properties.List); + TestHelpers.AssertExpression("test.properties.list", resource.Properties.List.ToBicepExpression()); + } + + [Test] + public void ValidateNestedListProperty_WithValues() + { + var resource = new TestResource("test") + { + Properties = new TestProperties() + }; + resource.Properties.List.Add("nestedItem1"); + resource.Properties.List.Add("nestedItem2"); + TestHelpers.AssertExpression(""" + [ + 'nestedItem1' + 'nestedItem2' + ] + """, resource.Properties.List); + TestHelpers.AssertExpression("test.properties.list", resource.Properties.List.ToBicepExpression()); + } + + [Test] + public void ValidateNestedListProperty_Indexer() + { + var resource = new TestResource("test") + { + Properties = new TestProperties() + }; + resource.Properties.List.Add("nestedItem1"); + var validIndexer = resource.Properties.List[0]; + TestHelpers.AssertExpression("'nestedItem1'", validIndexer); + TestHelpers.AssertExpression("test.properties.list[0]", validIndexer.ToBicepExpression()); + + var invalidIndexer = resource.Properties.List[1]; // out-of-range + Assert.Throws(() => invalidIndexer.ToString()); + TestHelpers.AssertExpression("test.properties.list[1]", invalidIndexer.ToBicepExpression()); + } + + [Test] + public void ValidateNestedOutputListProperty() + { + var resource = new TestResource("test") + { + Properties = new TestProperties() + }; + var nestedOutputList = resource.Properties.OutputList; + TestHelpers.AssertExpression("test.properties.outputList", nestedOutputList); + TestHelpers.AssertExpression("test.properties.outputList", nestedOutputList.ToBicepExpression()); + + // trying to modify this list should lead to exceptions + Assert.Throws(() => nestedOutputList.Add("item")); + Assert.Throws(() => nestedOutputList.Clear()); + } + + [Test] + public void ValidateNestedOutputListProperty_Indexer() + { + var resource = new TestResource("test") + { + Properties = new TestProperties() + }; + // add value to an output list will throw + Assert.Throws(() => resource.Properties.OutputList.Add("outputItem1")); + // call the setter of indexer will throw + Assert.Throws(() => resource.Properties.OutputList[0] = "outputItem1"); + + var validIndexer = resource.Properties.OutputList[0]; + Assert.Throws(() => validIndexer.ToString()); + TestHelpers.AssertExpression("test.properties.outputList[0]", validIndexer.ToBicepExpression()); + } + + [Test] + public void ValidateDictionaryProperty_Undefined() + { + var resource = new TestResource("test"); + TestHelpers.AssertExpression("test.dictionary", resource.Dictionary); + TestHelpers.AssertExpression("test.dictionary", resource.Dictionary.ToBicepExpression()); + } + + [Test] + public void ValidateDictionaryProperty_Empty_ExplicitCallClear() + { + var resource = new TestResource("test"); + resource.Dictionary.Clear(); // explicitly clear to make it empty + TestHelpers.AssertExpression("{ }", resource.Dictionary); + TestHelpers.AssertExpression("test.dictionary", resource.Dictionary.ToBicepExpression()); + } + + [Test] + public void ValidateDictionaryProperty_Empty_AssignNewDictionary() + { + var resource = new TestResource("test"); + resource.Dictionary = new BicepDictionary(); // assign a new empty dictionary + TestHelpers.AssertExpression("{ }", resource.Dictionary); + TestHelpers.AssertExpression("test.dictionary", resource.Dictionary.ToBicepExpression()); + } + + [Test] + public void ValidateDictionaryProperty_Empty_AddAndRemoveAll() + { + var resource = new TestResource("test"); + resource.Dictionary["key1"] = "value1"; + resource.Dictionary.Remove("key1"); // all the items are removed, now the dictionary is empty + TestHelpers.AssertExpression("{ }", resource.Dictionary); + TestHelpers.AssertExpression("test.dictionary", resource.Dictionary.ToBicepExpression()); + } + + [Test] + public void ValidateDictionaryProperty_WithValues() + { + var resource = new TestResource("test"); + resource.Dictionary["key1"] = "value1"; + resource.Dictionary["key2"] = "value2"; + TestHelpers.AssertExpression(""" + { + key1: 'value1' + key2: 'value2' + } + """, resource.Dictionary); + TestHelpers.AssertExpression("test.dictionary", resource.Dictionary.ToBicepExpression()); + } + + [Test] + public void ValidateDictionaryProperty_Indexer() + { + var resource = new TestResource("test"); + resource.Dictionary["key1"] = "value1"; + var validIndexer = resource.Dictionary["key1"]; + TestHelpers.AssertExpression("'value1'", validIndexer); + TestHelpers.AssertExpression("test.dictionary['key1']", validIndexer.ToBicepExpression()); + + var invalidIndexer = resource.Dictionary["missingKey"]; + Assert.Throws(() => invalidIndexer.ToString()); + TestHelpers.AssertExpression("test.dictionary['missingKey']", invalidIndexer.ToBicepExpression()); + } + + [Test] + public void ValidateOutputDictionaryProperty() + { + var resource = new TestResource("test"); + TestHelpers.AssertExpression("test.outputDictionary", resource.OutputDictionary); + TestHelpers.AssertExpression("test.outputDictionary", resource.OutputDictionary.ToBicepExpression()); + + // trying to modify this dictionary should lead to exceptions + Assert.Throws(() => resource.OutputDictionary.Add("key", "value")); + Assert.Throws(() => resource.OutputDictionary.Clear()); + Assert.Throws(() => resource.OutputDictionary.Remove("key")); + Assert.Throws(() => resource.OutputDictionary["key"] = "value"); + } + + [Test] + public void ValidateOutputDictionaryProperty_Indexer() + { + var resource = new TestResource("test"); + // add value to an output dictionary will throw + Assert.Throws(() => resource.OutputDictionary.Add("outputKey", "outputValue")); + // call the setter of indexer will throw + Assert.Throws(() => resource.OutputDictionary["outputKey"] = "outputValue"); + + var validIndexer = resource.OutputDictionary["outputKey"]; + Assert.Throws(() => validIndexer.ToString()); + TestHelpers.AssertExpression("test.outputDictionary['outputKey']", validIndexer.ToBicepExpression()); + } + + [Test] + public void ValidateNestedDictionaryProperty_Undefined() + { + var resource = new TestResource("test") + { + Properties = new TestProperties() + }; + TestHelpers.AssertExpression("test.properties.dictionary", resource.Properties.Dictionary); + TestHelpers.AssertExpression("test.properties.dictionary", resource.Properties.Dictionary.ToBicepExpression()); + } + + [Test] + public void ValidateNestedDictionaryProperty_Empty_ExplicitCallClear() + { + var resource = new TestResource("test") + { + Properties = new TestProperties() + }; + resource.Properties.Dictionary.Clear(); // explicitly clear to make it empty + TestHelpers.AssertExpression("{ }", resource.Properties.Dictionary); + TestHelpers.AssertExpression("test.properties.dictionary", resource.Properties.Dictionary.ToBicepExpression()); + } + + [Test] + public void ValidateNestedDictionaryProperty_Empty_AssignNewDictionary() + { + var resource = new TestResource("test") + { + Properties = new TestProperties() + }; + resource.Properties.Dictionary = new BicepDictionary(); // assign a new empty dictionary + TestHelpers.AssertExpression("{ }", resource.Properties.Dictionary); + TestHelpers.AssertExpression("test.properties.dictionary", resource.Properties.Dictionary.ToBicepExpression()); + } + + [Test] + public void ValidateNestedDictionaryProperty_Empty_AddAndRemoveAll() + { + var resource = new TestResource("test") + { + Properties = new TestProperties() + }; + resource.Properties.Dictionary["nestedKey1"] = "nestedValue1"; + resource.Properties.Dictionary.Remove("nestedKey1"); // all the items are removed, now the dictionary is empty + TestHelpers.AssertExpression("{ }", resource.Properties.Dictionary); + TestHelpers.AssertExpression("test.properties.dictionary", resource.Properties.Dictionary.ToBicepExpression()); + } + + [Test] + public void ValidateNestedDictionaryProperty_Empty_AddAndClear() + { + var resource = new TestResource("test") + { + Properties = new TestProperties() + }; + resource.Properties.Dictionary["nestedKey1"] = "nestedValue1"; + resource.Properties.Dictionary.Clear(); // all the items are removed, now the dictionary is empty + TestHelpers.AssertExpression("{ }", resource.Properties.Dictionary); + TestHelpers.AssertExpression("test.properties.dictionary", resource.Properties.Dictionary.ToBicepExpression()); + } + + [Test] + public void ValidateNestedDictionaryProperty_WithValues() + { + var resource = new TestResource("test") + { + Properties = new TestProperties() + }; + resource.Properties.Dictionary["nestedKey1"] = "nestedValue1"; + resource.Properties.Dictionary["nestedKey2"] = "nestedValue2"; + TestHelpers.AssertExpression(""" + { + nestedKey1: 'nestedValue1' + nestedKey2: 'nestedValue2' + } + """, resource.Properties.Dictionary); + TestHelpers.AssertExpression("test.properties.dictionary", resource.Properties.Dictionary.ToBicepExpression()); + } + + [Test] + public void ValidateNestedDictionaryProperty_Indexer() + { + var resource = new TestResource("test") + { + Properties = new TestProperties() + }; + resource.Properties.Dictionary["nestedKey1"] = "nestedValue1"; + var validIndexer = resource.Properties.Dictionary["nestedKey1"]; + TestHelpers.AssertExpression("'nestedValue1'", validIndexer); + TestHelpers.AssertExpression("test.properties.dictionary['nestedKey1']", validIndexer.ToBicepExpression()); + var invalidIndexer = resource.Properties.Dictionary["missingNestedKey"]; + Assert.Throws(() => invalidIndexer.ToString()); + TestHelpers.AssertExpression("test.properties.dictionary['missingNestedKey']", invalidIndexer.ToBicepExpression()); + } + + [Test] + public void ValidateNestedOutputDictionaryProperty() + { + var resource = new TestResource("test") + { + Properties = new TestProperties() + }; + TestHelpers.AssertExpression("test.properties.outputDictionary", resource.Properties.OutputDictionary); + TestHelpers.AssertExpression("test.properties.outputDictionary", resource.Properties.OutputDictionary.ToBicepExpression()); + + // trying to modify this dictionary should lead to exceptions + Assert.Throws(() => resource.Properties.OutputDictionary.Add("key", "value")); + Assert.Throws(() => resource.Properties.OutputDictionary.Clear()); + Assert.Throws(() => resource.Properties.OutputDictionary.Remove("key")); + Assert.Throws(() => resource.Properties.OutputDictionary["key"] = "value"); + } + + [Test] + public void ValidateNestedOutputDictionaryProperty_Indexer() + { + var resource = new TestResource("test") + { + Properties = new TestProperties() + }; + // add value to an output dictionary will throw + Assert.Throws(() => resource.Properties.OutputDictionary.Add("outputKey", "outputValue")); + // call the setter of indexer will throw + Assert.Throws(() => resource.Properties.OutputDictionary["outputKey"] = "outputValue"); + + var validIndexer = resource.Properties.OutputDictionary["outputKey"]; + Assert.Throws(() => validIndexer.ToString()); + TestHelpers.AssertExpression("test.properties.outputDictionary['outputKey']", validIndexer.ToBicepExpression()); + } + + [Test] + public void ValidateFailsForPropertyOnUnnamedConstruct() + { + var properties = new TestProperties(); + properties.WithValue = "foo"; + var withValue = properties.WithValue; + TestHelpers.AssertExpression("'foo'", withValue); + Assert.Throws(() => withValue.ToBicepExpression().ToString()); + + var withoutValue = properties.WithoutValue; + Assert.Throws(() => withoutValue.ToString()); + Assert.Throws(() => withoutValue.ToBicepExpression().ToString()); + } + + [Test] + public void ValidateBicepFunctionInterpolate_PlainValues() + { + var resource = new TestResource("test"); + resource.WithValue = "foo"; + + var interpolated = BicepFunction.Interpolate($"with value: {resource.WithValue}, without value: {resource.WithoutValue}"); + + TestHelpers.AssertExpression("'with value: foo, without value: ${test.withoutValue}'", interpolated); + + var interpolatedWithExpressions = BicepFunction.Interpolate($"with value: {resource.WithValue.ToBicepExpression()}, without value: {resource.WithoutValue.ToBicepExpression()}"); + + TestHelpers.AssertExpression("'with value: ${test.withValue}, without value: ${test.withoutValue}'", interpolatedWithExpressions); + } + + [Test] + public void ValidateBicepFunctionInterpolate_ListValues() + { + var resource = new TestResource("test"); + resource.List.Add("item1"); + + var interpolated = BicepFunction.Interpolate($"list item: {resource.List[0]}"); + TestHelpers.AssertExpression("'list item: item1'", interpolated); + var interpolatedWithExpression = BicepFunction.Interpolate($"list item: {resource.List[0].ToBicepExpression()}"); + TestHelpers.AssertExpression("'list item: ${test.list[0]}'", interpolatedWithExpression); + // this should throw because `resource.List[1]` is out of range + Assert.Throws(() => BicepFunction.Interpolate($"list item with valid index: {resource.List[0]}, list item with invalid index: {resource.List[1]}")); + + var interpolatedExpressionWithInvalidIndex = BicepFunction.Interpolate($"list item with valid index: {resource.List[0].ToBicepExpression()}, list item with invalid index: {resource.List[1].ToBicepExpression()}"); + TestHelpers.AssertExpression("'list item with valid index: ${test.list[0]}, list item with invalid index: ${test.list[1]}'", interpolatedExpressionWithInvalidIndex); + } + + [Test] + public void ValidateBicepFunctionInterpolate_DictionaryValues() + { + var resource = new TestResource("test"); + resource.Dictionary["key1"] = "value1"; + + var interpolated = BicepFunction.Interpolate($"dictionary item: {resource.Dictionary["key1"]}"); + TestHelpers.AssertExpression("'dictionary item: value1'", interpolated); + + var interpolatedWithExpression = BicepFunction.Interpolate($"dictionary item: {resource.Dictionary["key1"].ToBicepExpression()}"); + TestHelpers.AssertExpression("'dictionary item: ${test.dictionary[\'key1\']}'", interpolatedWithExpression); + + // this should throw because `resource.Dictionary["missingKey"]` doesn't exist + Assert.Throws(() => BicepFunction.Interpolate($"dictionary item with valid key: {resource.Dictionary["key1"]}, dictionary item with missing key: {resource.Dictionary["missingKey"]}")); + + var interpolatedExpressionWithMissingKey = BicepFunction.Interpolate($"dictionary item with valid key: {resource.Dictionary["key1"].ToBicepExpression()}, dictionary item with missing key: {resource.Dictionary["missingKey"].ToBicepExpression()}"); + TestHelpers.AssertExpression("'dictionary item with valid key: ${test.dictionary[\'key1\']}, dictionary item with missing key: ${test.dictionary[\'missingKey\']}'", interpolatedExpressionWithMissingKey); + } + + [Test] + public void ValidateBicepFunctionInterpolate_NestedDictionaryValues() + { + var resource = new TestResource("test") + { + Properties = new TestProperties() + }; + resource.Properties.Dictionary["nestedKey1"] = "nestedValue1"; + + var interpolated = BicepFunction.Interpolate($"nested dictionary item: {resource.Properties.Dictionary["nestedKey1"]}"); + TestHelpers.AssertExpression("'nested dictionary item: nestedValue1'", interpolated); + + var interpolatedWithExpression = BicepFunction.Interpolate($"nested dictionary item: {resource.Properties.Dictionary["nestedKey1"].ToBicepExpression()}"); + TestHelpers.AssertExpression("'nested dictionary item: ${test.properties.dictionary[\'nestedKey1\']}'", interpolatedWithExpression); + + // this should throw because the missing key doesn't exist + Assert.Throws(() => BicepFunction.Interpolate($"nested dictionary item with valid key: {resource.Properties.Dictionary["nestedKey1"]}, nested dictionary item with missing key: {resource.Properties.Dictionary["missingNestedKey"]}")); + + var interpolatedExpressionWithMissingKey = BicepFunction.Interpolate($"nested dictionary item with valid key: {resource.Properties.Dictionary["nestedKey1"].ToBicepExpression()}, nested dictionary item with missing key: {resource.Properties.Dictionary["missingNestedKey"].ToBicepExpression()}"); + TestHelpers.AssertExpression("'nested dictionary item with valid key: ${test.properties.dictionary[\'nestedKey1\']}, nested dictionary item with missing key: ${test.properties.dictionary[\'missingNestedKey\']}'", interpolatedExpressionWithMissingKey); + } + + [Test] + public void ValidateBicepFunctionInterpolate_OutputDictionaryValues() + { + var resource = new TestResource("test"); + var outputDict = resource.OutputDictionary; + + // add value to an output dictionary will throw, so we can't populate it + Assert.Throws(() => resource.OutputDictionary.Add("outputKey", "outputValue")); + + // test direct reference to output dictionary key (which should behave like expressions) + var validIndexer = resource.OutputDictionary["outputKey"]; + Assert.Throws(() => validIndexer.ToString()); + + var interpolatedWithExpression = BicepFunction.Interpolate($"output dictionary item: {resource.OutputDictionary["outputKey"].ToBicepExpression()}"); + TestHelpers.AssertExpression("'output dictionary item: ${test.outputDictionary[\'outputKey\']}'", interpolatedWithExpression); + } + + [Test] + public void ValidateBicepFunctionInterpolate_MixedDictionaryAndListValues() + { + var resource = new TestResource("test"); + resource.Dictionary["config"] = "production"; + resource.List.Add("item1"); + + var interpolated = BicepFunction.Interpolate($"Config: {resource.Dictionary["config"]}, First item: {resource.List[0]}"); + TestHelpers.AssertExpression("'Config: production, First item: item1'", interpolated); + + var interpolatedWithExpressions = BicepFunction.Interpolate($"Config: {resource.Dictionary["config"].ToBicepExpression()}, First item: {resource.List[0].ToBicepExpression()}"); + TestHelpers.AssertExpression("'Config: ${test.dictionary[\'config\']}, First item: ${test.list[0]}'", interpolatedWithExpressions); + } + + [Test] + public void ValidateBicepFunctionInterpolate_DictionaryWithSpecialCharacterKeys() + { + var resource = new TestResource("test"); + resource.Dictionary["my-key"] = "my-value"; + resource.Dictionary["key.with.dots"] = "dotted-value"; + resource.Dictionary["key with spaces"] = "spaced-value"; + + var interpolated = BicepFunction.Interpolate($"Hyphenated: {resource.Dictionary["my-key"]}, Dotted: {resource.Dictionary["key.with.dots"]}, Spaced: {resource.Dictionary["key with spaces"]}"); + TestHelpers.AssertExpression("'Hyphenated: my-value, Dotted: dotted-value, Spaced: spaced-value'", interpolated); + + var interpolatedWithExpressions = BicepFunction.Interpolate($"Hyphenated: {resource.Dictionary["my-key"].ToBicepExpression()}, Dotted: {resource.Dictionary["key.with.dots"].ToBicepExpression()}, Spaced: {resource.Dictionary["key with spaces"].ToBicepExpression()}"); + TestHelpers.AssertExpression("'Hyphenated: ${test.dictionary[\'my-key\']}, Dotted: ${test.dictionary[\'key.with.dots\']}, Spaced: ${test.dictionary[\'key with spaces\']}'", interpolatedWithExpressions); + } + + private class TestResource : ProvisionableResource + { + public TestResource(string identifier) : base(identifier, "Microsoft.Tests/tests", "2025-11-09") + { + } + + private BicepValue? _withValue; + public BicepValue WithValue + { + get { Initialize(); return _withValue!; } + set { Initialize(); _withValue!.Assign(value); } + } + + private BicepValue? _withoutValue; + public BicepValue WithoutValue + { + get { Initialize(); return _withoutValue!; } + set { Initialize(); _withoutValue!.Assign(value); } + } + + private BicepValue? _outputValue; + public BicepValue OutputValue + { + get { Initialize(); return _outputValue!; } + } + + private OutputModel? _outputModel; + public OutputModel OutputModel + { + get { Initialize(); return _outputModel!; } + } + + private BicepList? _list; + public BicepList List + { + get { Initialize(); return _list!; } + set { Initialize(); _list!.Assign(value); } + } + + private BicepList? _outputList; + public BicepList OutputList + { + get { Initialize(); return _outputList!; } + } + + private BicepDictionary? _dictionary; + public BicepDictionary Dictionary + { + get { Initialize(); return _dictionary!; } + set { Initialize(); _dictionary!.Assign(value); } + } + + private BicepDictionary? _outputDictionary; + public BicepDictionary OutputDictionary + { + get { Initialize(); return _outputDictionary!; } + } + + private BicepList? _models; + public BicepList Models + { + get { Initialize(); return _models!; } + set { Initialize(); _models!.Assign(value); } + } + + private TestProperties? _properties; + public TestProperties Properties + { + get { Initialize(); return _properties!; } + set { Initialize(); AssignOrReplace(ref _properties, value); } + } + + protected override void DefineProvisionableProperties() + { + base.DefineProvisionableProperties(); + _withValue = DefineProperty("WithValue", ["withValue"]); + _withoutValue = DefineProperty("WithoutValue", ["withoutValue"]); + _outputValue = DefineProperty("OutputValue", ["outputValue"], isOutput: true); + _outputModel = DefineModelProperty("OutputModel", ["outputModel"], isOutput: true); + _list = DefineListProperty("List", ["list"]); + _outputList = DefineListProperty("OutputList", ["outputList"], isOutput: true); + _models = DefineListProperty("Models", ["models"]); + _dictionary = DefineDictionaryProperty("Dictionary", ["dictionary"]); + _outputDictionary = DefineDictionaryProperty("OutputDictionary", ["outputDictionary"], isOutput: true); + _properties = DefineModelProperty("Properties", ["properties"]); + } + } + + private class TestProperties : ProvisionableConstruct + { + private BicepValue? _withValue; + public BicepValue WithValue + { + get { Initialize(); return _withValue!; } + set { Initialize(); _withValue!.Assign(value); } + } + + private BicepValue? _withoutValue; + public BicepValue WithoutValue + { + get { Initialize(); return _withoutValue!; } + set { Initialize(); _withoutValue = value; } + } + + private BicepList? _list; + public BicepList List + { + get { Initialize(); return _list!; } + set { Initialize(); _list!.Assign(value); } + } + + private BicepList? _outputList; + public BicepList OutputList + { + get { Initialize(); return _outputList!; } + } + + private BicepDictionary? _dictionary; + public BicepDictionary Dictionary + { + get { Initialize(); return _dictionary!; } + set { Initialize(); _dictionary!.Assign(value); } + } + + private BicepDictionary? _outputDictionary; + public BicepDictionary OutputDictionary + { + get { Initialize(); return _outputDictionary!; } + } + + protected override void DefineProvisionableProperties() + { + base.DefineProvisionableProperties(); + _withValue = DefineProperty("WithValue", ["withValue"]); + _withoutValue = DefineProperty("WithoutValue", ["withoutValue"]); + _list = DefineListProperty("List", ["list"]); + _outputList = DefineListProperty("OutputList", ["outputList"], isOutput: true); + _dictionary = DefineDictionaryProperty("Dictionary", ["dictionary"]); + _outputDictionary = DefineDictionaryProperty("OutputDictionary", ["outputDictionary"], isOutput: true); + } + } + + private class OutputModel : ProvisionableConstruct + { + private BicepValue? _id; + public BicepValue Id + { + get { Initialize(); return _id!; } + } + + protected override void DefineProvisionableProperties() + { + base.DefineProvisionableProperties(); + _id = DefineProperty("Id", ["id"], isOutput: true); + } + } + + private class TestModel : ProvisionableConstruct + { + private BicepValue? _name; + public BicepValue Name + { + get { Initialize(); return _name!; } + set { Initialize(); _name!.Assign(value); } + } + protected override void DefineProvisionableProperties() + { + base.DefineProvisionableProperties(); + _name = DefineProperty("Name", ["name"]); + } + } + } +} diff --git a/sdk/provisioning/Azure.Provisioning/tests/Primitives/ProvisionableResourceTests.cs b/sdk/provisioning/Azure.Provisioning/tests/Primitives/ProvisionableResourceTests.cs index a7da1b677023..4f23ea2ac8a9 100644 --- a/sdk/provisioning/Azure.Provisioning/tests/Primitives/ProvisionableResourceTests.cs +++ b/sdk/provisioning/Azure.Provisioning/tests/Primitives/ProvisionableResourceTests.cs @@ -161,6 +161,102 @@ public async Task ValidateListProperties_Unset() """); } + [Test] + public async Task ValidateListProperties_ForceEmpty() + { + await using var test = new Trycep(); + + test.Define( + ctx => + { + Infrastructure infra = new(); + + // Create our comprehensive test resource with various property types + var storageAccount = new StorageAccount("storageAccount") + { + // Test basic string property + Name = "test-storage", + + // Test location property + Location = AzureLocation.WestUS2, + + // Test enum property + StorageTier = StorageTier.Standard, + + // Force empty list properties + AllowedSubnets = [], + }; + + // Clear would also mark the list as empty + storageAccount.DeniedPorts.Clear(); + + infra.Add(storageAccount); + return infra; + }) + .Compare( + """ + resource storageAccount 'Test.Provider/storageAccounts@2024-01-01' = { + name: 'test-storage' + location: 'westus2' + properties: { + tier: 'Standard' + allowedSubnets: [] + deniedPorts: [] + } + } + """); + } + + [Test] + public async Task ValidateListProperties_ForceEmptyThenAdd() + { + await using var test = new Trycep(); + + test.Define( + ctx => + { + Infrastructure infra = new(); + + // Create our comprehensive test resource with various property types + var storageAccount = new StorageAccount("storageAccount") + { + // Test basic string property + Name = "test-storage", + + // Test location property + Location = AzureLocation.WestUS2, + + // Test enum property + StorageTier = StorageTier.Standard, + + // Force empty list properties + AllowedSubnets = [], + }; + + // Clear would also mark the list as empty + storageAccount.DeniedPorts.Clear(); + + storageAccount.AllowedSubnets.Add("192.168.1.0/24"); + + infra.Add(storageAccount); + return infra; + }) + .Compare( + """ + resource storageAccount 'Test.Provider/storageAccounts@2024-01-01' = { + name: 'test-storage' + location: 'westus2' + properties: { + tier: 'Standard' + allowedSubnets: [ + '192.168.1.0/24' + ] + deniedPorts: [] + } + } + """); + } + [Test] public async Task ValidateResourceReference() { @@ -411,6 +507,13 @@ public BicepList AllowedSubnets } private BicepList? _allowedSubnets; + public BicepList DeniedPorts + { + get { Initialize(); return _deniedPorts!; } + set { Initialize(); _deniedPorts!.Assign(value); } + } + private BicepList? _deniedPorts; + // List of models property public BicepList AccessRules { @@ -451,6 +554,7 @@ protected override void DefineProvisionableProperties() _storageConfiguration = DefineModelProperty("StorageConfiguration", ["properties", "storageConfiguration"]); _tags = DefineDictionaryProperty("Tags", ["tags"]); _allowedSubnets = DefineListProperty("AllowedSubnets", ["properties", "allowedSubnets"]); + _deniedPorts = DefineListProperty("DeniedPorts", ["properties", "deniedPorts"]); _accessRules = DefineListProperty("AccessRules", ["properties", "accessRules"]); // Output-only properties diff --git a/sdk/provisioning/Azure.Provisioning/tests/ReadmeSnippets/BicepValueTypesSnippets.cs b/sdk/provisioning/Azure.Provisioning/tests/ReadmeSnippets/BicepValueTypesSnippets.cs new file mode 100644 index 000000000000..148af2f2e414 --- /dev/null +++ b/sdk/provisioning/Azure.Provisioning/tests/ReadmeSnippets/BicepValueTypesSnippets.cs @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Provisioning.Expressions; +using Azure.Provisioning.Storage; +using NUnit.Framework; + +namespace Azure.Provisioning.Tests.ReadmeSnippets; + +internal class BicepValueTypesSnippets +{ + [Test] + [Ignore("Ignore this since this is a snippet instead of a test")] + public void BicepValueTypes_ThreeKindsOfBicepValue() + { + StorageAccount storageAccount = new(nameof(storageAccount)); + #region Snippet:ThreeKindsOfBicepValue + BicepValue literalName = "my-storage-account"; + + // Expression value + BicepValue expressionName = BicepFunction.CreateGuid(); + + // Unset value (can be assigned later) + BicepValue unsetName = storageAccount.Name; + #endregion + } + + [Test] + [Ignore("Ignore this since this is a snippet instead of a test")] + public void BicepValueTypes_BicepListUsages() + { + #region Snippet:BicepListUsages + // Literal list + BicepList tagNames = new() { "Environment", "Project", "Owner" }; + + // 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 + + // Expression list (referencing a parameter) + ProvisioningParameter parameter = new(nameof(parameter), typeof(string[])); + BicepList dynamicTags = parameter; + #endregion + } + + [Test] + [Ignore("Ignore this since this is a snippet instead of a test")] + public void BicepValueTypes_BicepDictionaryUsages() + { + #region Snippet:BicepDictionaryUsages + // Literal dictionary + BicepDictionary tags = new() + { + ["Environment"] = "Production", + ["Project"] = "WebApp", + ["Owner"] = "DevTeam" + }; + + // Accessing values + tags["CostCenter"] = "12345"; + + // Expression dictionary + ProvisioningParameter parameter = new(nameof(parameter), typeof(object)); + BicepDictionary dynamicTags = parameter; + #endregion + } + + [Test] + [Ignore("Ignore this since this is a snippet instead of a test")] + public void BicepValueTypes_WorkingWithAzureResources() + { + #region 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 myStorage = new(nameof(myStorage), StorageAccount.ResourceVersions.V2023_01_01) + { + // Set literal values + Name = "mystorageaccount", + Kind = StorageKind.StorageV2, + + // Use BicepValue for dynamic configuration + Location = location, // Reference a parameter + + // Configure nested properties + Sku = new StorageSku + { + Name = StorageSkuName.StandardLrs + }, + + // Use BicepList for collections + Tags = new BicepDictionary + { + ["Environment"] = "Production", + ["Project"] = environment // Mix literal and dynamic values + } + }; + + // Access output properties and use them in output (these are BicepValue 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 + }; + #endregion + } +} diff --git a/sdk/provisioning/Azure.Provisioning/tests/ReadmeSnippets/ImportantUsageGuideLinesSnippets.cs b/sdk/provisioning/Azure.Provisioning/tests/ReadmeSnippets/ImportantUsageGuideLinesSnippets.cs new file mode 100644 index 000000000000..4530f1fb3f31 --- /dev/null +++ b/sdk/provisioning/Azure.Provisioning/tests/ReadmeSnippets/ImportantUsageGuideLinesSnippets.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Provisioning.CognitiveServices; +using Azure.Provisioning.Expressions; +using Azure.Provisioning.Storage; +using NUnit.Framework; + +namespace Azure.Provisioning.Tests.ReadmeSnippets; + +internal class ImportantUsageGuideLinesSnippets +{ + [Test] + [Ignore("Ignore this since this is a snippet instead of a test")] + public void ImportantUsageGuidelines_CreateSeparateInstances() + { + #region 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 } + }; + #endregion + } + + [Test] + [Ignore("Ignore this since this is a snippet instead of a test")] + public void ImportantUsageGuidelines_ReuseInstances() + { + #region 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 + #endregion + } + + [Test] + [Ignore("Ignore this since this is a snippet instead of a test")] + public void ImportantUsageGuidelines_SafeCollectionAccess() + { + #region 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 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 actualValue = apiEndpoint.Value; // Would throw KeyNotFoundException at runtime + #endregion + } +} diff --git a/sdk/provisioning/Azure.Provisioning/tests/ReadmeSnippets/ToBicepExpressionMethodSnippets.cs b/sdk/provisioning/Azure.Provisioning/tests/ReadmeSnippets/ToBicepExpressionMethodSnippets.cs new file mode 100644 index 000000000000..d6f3277115be --- /dev/null +++ b/sdk/provisioning/Azure.Provisioning/tests/ReadmeSnippets/ToBicepExpressionMethodSnippets.cs @@ -0,0 +1,106 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Provisioning.Expressions; +using Azure.Provisioning.Storage; +using NUnit.Framework; + +namespace Azure.Provisioning.Tests.ReadmeSnippets; + +internal class ToBicepExpressionMethodSnippets +{ + [Test] + [Ignore("Ignore this since this is a snippet instead of a test")] + public void ToBicepExpression_CommonUseCases() + { + #region 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 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 nonExpressionConnectionString = + BicepFunction.Interpolate( + $"AccountName={storage.Name};EndpointSuffix=core.windows.net" + ); + // this would produce: 'AccountName=mystorageaccount;EndpointSuffix=core.windows.net' + #endregion + } + + [Test] + [Ignore("Ignore this since this is a snippet instead of a test")] + public void ToBicepExpression_NamedProvisionableConstructRequirement() + { + #region 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 + #endregion + } + + [Test] + [Ignore("Ignore this since this is a snippet instead of a test")] + public void ToBicepExpression_InstanceSharingCorrect() + { + #region Snippet:InstanceSharingCorrect + // ✅ GOOD: Create separate instances with the same values + StorageAccount storage1 = new("storage1") + { + 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}" + #endregion + } + + [Test] + [Ignore("Ignore this since this is a snippet instead of a test")] + public void ToBicepExpression_InstanceSharingProblem() + { + #region 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. + #endregion + } +}