Set or skip an object property based on a condition
Problem statement
This issue exists generally to track the requirement of being able to conditionally set a property on an object. This is often necessary, because although when authoring in Bicep we don't distinguish between the absence of a value and explicitly setting a value to null, some of the services which handle resource deployment (resource providers) do.
For example, it might be desirable to conditionally not set a property:
param setFooProp bool
resource foo '<type>' = {
name: 'foo'
properties: {
fooProp: setFooProp ? 'bar' : null
barProp: 'baz'
}
}
However, if setFooProp is false, this is equivalent to writing:
resource foo '<type>' = {
name: 'foo'
properties: {
fooProp: null
barProp: 'baz'
}
}
Whereas the author may instead intend to have the following instead:
resource foo '<type>' = {
name: 'foo'
properties: {
barProp: 'baz'
}
}
To accomplish the latter option, a more significant refactor is needed. We support this with the spread operator:
resource foo '<type>' = {
name: 'foo'
properties: {
...(setFooProp ? { fooProp: 'bar' } : {})
barProp: 'baz'
}
}
Or by refactoring into a variable:
var fooProps = setFooProp ? {
fooProp: 'bar'
} : {}
resource foo '<type>' = {
name: 'foo'
properties: {
...fooProps
barProp: 'baz'
}
}
Why is this a new issue?
The requirement for "set-or-skip" was originally being tracked by #387, which is now over 4 years old, and is one of our highest-upvoted issue with many comments. Since then, we've implemented the spread operator (usage docs).
The requirements of the original issue are satisfied with the spread operator, but we wanted to continue to collect feedback about set-or-skip, because of the popularity of the thread. We are having a hard time prioritizing this work because we're not sure whether:
- This is still important to people (we can't tell whether the upvotes on the issue were before or after spread was released!)
- The spread operator is too complex to use
- The spread operator isn't well known or difficult to discover (are there docs improvements to be made?)
- The spread operator has feature gaps
- ...something else?
As such we have closed #387 but intend to keep this issue open to track new feedback. Please comment on this issue if you'd like to help us answer the above!
Special-case syntax
Under #387, the original discussion was mostly around introducing a new dedicated syntax - here is a prototype of the syntax example for "set-or-skip" syntax that was being discussed. This is not implemented, and is not something we are currently considering without clear feedback supporting it:
var obj = {
propNormal: value
if (condition) {
propConditional1: propConditional1Value
propConditional2: propConditional2Value
}
}
var arr = [
element
if (condition) { condElement1, condElement2 }
]
So what is the goal of this issue - just to collect feedback?
My 2 cents:
- My upvote on the previous issue was after spread was released.
- "The spread operator isn't well known or difficult to discover" IMO, yes. I didn't know spread exists until just now; I had no trigger to think it might be used to solve this problem. When searching the internet, I did land on the "set or skip" github issue. I did not land on this particular example in the bicep spread documentation page.
- "The spread operator is too complex to use"
Debatable, but IMO yes. If I didn't already know this usage pattern, then if I saw some bicep code with
...(condition ? value : blank), I don't think I would immediately understand that its purpose is "conditionally set or omit property".
Maybe the bicep spread docs can be modified to more explicitly showcase the "conditionally set property on object" / "conditionally add element to array" usage pattern? I.e. make it more discoverable via internet search (SEO). https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/operator-spread
Maybe add a note to the docs pages for if and ternary ? :, mentioning that you cannot use them for these use cases, that you need to use ... spread instead?
(After discovering the "set or skip" unresolved issue, I settled on a union() with a ternary ? : instead.)
@odegroot - thank you for the detailed feedback! I've created #15499 to cover your comments on documentation, and have reworded the original issue text to try and explain the purpose more clearly.
@anthony-c-martin, this is probably one of the items that I struggle with the most in bicep, so conversation and forthcoming changes are definitely welcome. Thank you!
I'm wondering how this would affect code such as this. Take an array with X elements and test for a condition to only assign a property to a properties array in a resource when a condition is matched. I essentially want to drop the else from the ternary. Creating an empty object does not get dropped, it seems. Using null causes the linter to trip.
properties: [for (config) in myArray: (config.foo == "bar") ? {
id: config.Id
} : {}
]
@anthony-c-martin, this is probably one of the items that I struggle with the most in bicep, so conversation and forthcoming changes are definitely welcome. Thank you!
I'm wondering how this would affect code such as this. Take an array with X elements and test for a condition to only assign a property to a
propertiesarray in a resource when a condition is matched. I essentially want to drop theelsefrom the ternary. Creating an empty object does not get dropped, it seems. Usingnullcauses the linter to trip.properties: [for (config) in myArray: (config.foo == "bar") ? { id: config.Id } : {} ]
@simonkurtz-MSFT I think the filter function is what you're looking for - it allows you to only select specific records from an array by a condition.
Here's how I would use it in your example:
properties: [for config in filter(myArray, config => config.foo == 'bar'): {
id: config.id
}]
Or if you want to use map instead of the for loop:
properties: map(
filter(myArray, config => config.foo == 'bar'),
config => { id: config.id })
Hi @anthony-c-martin, thank you so much! I'll give this a try when I'm back in the office next week. Many thanks!
I've stumbled upon this issue when trying to conditionally add ResourceAccessRules on Microsoft.Storage/storageAccounts NetworkAcls property. I found that the spread-operator did work in this case.
However, as mentioned before I think, not all Resource Providers act the same on null values for resource properties.
For example, when dealing with deployments for Data Factory where sometimes an existing Factory is configured with a Repository, I did not manage to exclude RepositoryConfiguration property from the deployment. It kept on removing the Repo config no matter what I tried.
One related use case that the spread operator seems to be able to solve, is AVM Bicep modules where one try to modernize existing IaC + resource providers handling NULL differently + AVM setting properties that did not exist originally to NULL. Ref:
I had problems with storage account and isHnsEnabled:
Looking at the storage account module, this problem seem to persist. Even though the spread operator is used other places in AVM.
- https://github.com/Azure/bicep-registry-modules/blob/main/avm/res/storage/storage-account/main.bicep
My linked use case (URL number 2) would still be blocked, but could be fixed in AVM with .... Correct?
I have an example with MS Graph Bicep that does not allow me to use the spread operator at the resource root level which is a problem because most properties in MS Graph resources are at the root. Is there any way around this?
param appRoleAssignmentRequired bool?
param tags array?
resource spApp 'Microsoft.Graph/[email protected]' = {
appId: appId
...(tags != null ? { tags: tags } : {})
...(appRoleAssignmentRequired != null ? { appRoleAssignmentRequired: appRoleAssignmentRequired } : {})
}
One workaround I was thinking about would be to have a module read the existing value and then call another module that then sets the updated value else set the previous value again. I have used this approach to handle things like appending app settings on app service without removing app settings that are not on the list. This could work but it is not pretty and could have side-effects where the value was read but a second outside process updates the value before bicep writes the value back, effectively reverting the change from the outside process.