Skip to content

Introduce a bicep reference builder #51370

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 19 commits into
base: main
Choose a base branch
from

Conversation

ArcturusZhang
Copy link
Member

@ArcturusZhang ArcturusZhang commented Jul 21, 2025

Fixes #51371
Fixes #48491

This PR introduces two extension methods:

public static BicepExpression ToBicepExpression(this IBicepValue value);
public static BicepExpression ToBicepExpression<T>(this BicepValue<T> value);

which converts a bicep value instance into an expression.

Also fixes the issue that when we try to use out-of-range index on a list or a non-exist key on a dictionary, the ArgumentOutOfRangeException of KeyNotFoundException are thrown.
Now the exception are deferred to throw until we are trying to access the value in it, but if we convert it to an expression, no exception would be thrown at all.

Contributing to the Azure SDK

Please see our CONTRIBUTING.md if you are not familiar with contributing to this repository or have questions.

For specific information about pull request etiquette and best practices, see this section.

Copy link

github-actions bot commented Jul 21, 2025

API Change Check

APIView identified API level changes in this PR and created the following API reviews

Azure.Provisioning

@ArcturusZhang ArcturusZhang marked this pull request as ready for review July 22, 2025 04:14
@Copilot Copilot AI review requested due to automatic review settings July 22, 2025 04:14
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR introduces a bicep reference builder that enables generating proper Bicep expressions from BicepValue objects, addressing issue #51371. The implementation provides a way to convert BicepValue instances to their corresponding Bicep reference expressions rather than their literal values.

Key changes include:

  • Added ToBicepExpression extension methods for converting BicepValue objects to Bicep reference expressions
  • Implemented specialized indexer classes for list and dictionary access patterns
  • Enhanced list and dictionary indexers to handle out-of-bounds and missing key scenarios properly

Reviewed Changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
BicepValueReferenceExtensions.cs Core extension methods for converting BicepValue to BicepExpression references
BicepListIndexer.cs Internal class handling list indexer references with proper error handling
BicepDictionaryIndexer.cs Internal class handling dictionary indexer references with proper error handling
BicepListOfT.cs Enhanced indexer logic to return proper reference types and validate bounds
BicepDictionaryOfT.cs Enhanced dictionary indexer to handle missing keys and return reference types
BicepValue.cs Made Compile method virtual to allow specialized indexer behavior
BicepValueReferenceExtensionsTests.cs Comprehensive test suite covering all reference generation scenarios

ArcturusZhang and others added 6 commits July 22, 2025 12:19
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…ueReferenceExtensionsTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Comment on lines 636 to 637
public static Azure.Provisioning.Expressions.BicepExpression ToBicepExpression(this Azure.Provisioning.IBicepValue value) { throw null; }
public static Azure.Provisioning.Expressions.BicepExpression ToBicepExpression<T>(this Azure.Provisioning.BicepValue<T> value) { throw null; }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all the properties with "primitive" types would be of a type of BicepValue<T>.
The IBicepValue overload is here to take up the cases for BicepList<T>, BicepDictionary<T>, ProvisionableConstruct and other stuffs that I did not notice.
Should we change this to a set of more explicit overloads that explicitly takes the above variants?

@@ -75,7 +75,7 @@ internal BicepValue() { }
public virtual bool IsEmpty { get { throw null; } }
void Azure.Provisioning.IBicepValue.Assign(Azure.Provisioning.IBicepValue source) { }
void Azure.Provisioning.IBicepValue.SetReadOnly() { }
public Azure.Provisioning.Expressions.BicepExpression Compile() { throw null; }
public virtual Azure.Provisioning.Expressions.BicepExpression Compile() { throw null; }
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the implementation, you could see I add this virtual to make sure we could be possible to throw the exception when the item gets compiled.
We could add a private proected virtual BicepExpression CompileCore() in order not to change public API.

@ArcturusZhang
Copy link
Member Author

ArcturusZhang commented Jul 24, 2025

I notice that this implementation might have a challenge in the case that we use BicepFunction.Interpolate(BicepInterpolatedStringHandler).

or maybe not - we could say, if you want one of the argument in your interpolated string to be an expression, you need to call the method and use the output expression as the argument.
The above concern is that would it be possible that we could have something to wrap the interpolated result and wrap all its argument into expression automatically

}
set
{
if (_kind == BicepValueKind.Expression || _isOutput)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what happened before if someone tried to set such a value?

Copy link
Member Author

@ArcturusZhang ArcturusZhang Aug 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

they will succeed at setting it.
You could refer the implementation of BicepList<T> - it has this check therefore if they tried to

list[0] = ...

on an output property, it would throw.

// ensure there is a value
if (index >= _values.Count)
{
return new BicepListIndexer<T>(_self, index);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is it safe to assume that if you pass in an index that is too big you meant to get back something that is only present at runtime? I am not sure this assumption is safe here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well this is the part that I once said "the exceptions are deferred".

Here are possible scenarios:

  1. the list is not output only, and the user used a "valid" index: everything works just fine.
  2. the list is output, and the user used an index to reference an item:
    when the list is output, no one could add items into it, therefore the index must be out of range here - we will always go into this branch.
  3. the list is not output, and the user used a "valid" index: everything works just fine.
  4. the list is not output, and the user used a "invalid" index: now we do not have a value that we could get from the list. Therefore we go into this branch.

In the 2 cases that we go into this branch, if the user wants to get the value, we throw because there is no such a value. If they want an expression, I think the best we could do is to spit out the expression and let ARM to throw the error, we do not have a way to know if this is valid.

/// <returns></returns>
public static BicepExpression ToBicepExpression<T>(this BicepValue<T> value)
{
if (value is BicepListIndexer<T> indexer)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we use a switch here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will fix

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Properties = new TestProperties()
};
var nestedOutputList = resource.Properties.OutputList;
Assert.AreEqual("[]", nestedOutputList.ToString()); // TODO -- should this throw? or just like right now we return an empty list?
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@m-nash what do you think about this?

public void Add(BicepValue<T> item) => _values.Add(item);
public void Insert(int index, BicepValue<T> item)
{
if (_kind == BicepValueKind.Expression || _isOutput)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let us revert these changes on throwing exceptions

Comment on lines +107 to +114
case BicepValueKind.Unset:
return new BicepListIndexer<T>(_self, index);
case BicepValueKind.Expression:
return new BicepListIndexer<T>(_self, index, value.Expression!);
case BicepValueKind.Literal:
return new BicepListIndexer<T>(_self, index, (T)value.LiteralValue!);
default:
throw new InvalidOperationException($"Unknown {value.Kind}!");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change one of these to default

throw new ArgumentOutOfRangeException(nameof(index), "Index must be non-negative.");
}
// ensure there is a value
if (index >= _values.Count)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how would this works on a list of models?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

var x = resource.Models[1];
var xName = x.Name;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
2 participants