Allow building keyed objects from module-loop outputs (workaround for BCP247 in toObject / object outputs)
- Dominant language
- Bicep
- Stars
- 3.6k
- Forks
- 830
- Avg merge
- 1d 2h
- Merged PRs (30d)
- 79
Description
### Summary
There is no clean way in Bicep to expose the outputs of a for-loop module collection as an object keyed by a stable alias. The two natural shapes both hit dead ends:
1. `toObject` over the source array fails with **BCP247** because the value selector necessarily indexes the module collection with the lambda variable:
```bicep
var zones = [
{ key: 'blob', name: 'privatelink.blob.core.windows.net' }
{ key: 'servicebus', name: 'privatelink.servicebus.windows.net' }
]
module dnsZones 'br/public:avm/res/network/private-dns-zone:0.7.0' = [
for z in zones: {
name: 'dnsZone-${z.key}'
params: { name: z.name, location: 'global', virtualNetworkLinks: [] }
}
]
// BCP247: Using lambda variables inside resource or module array access is not currently supported.
output dnsZoneIds object = toObject(
zones,
z => z.key,
z => dnsZones[indexOf(map(zones, x => x.key), z.key)].outputs.resourceId
)
```
2. Object comprehension is not supported by the language:
```bicep
output dnsZoneIds object = { for z in dnsZones: z.key: z.outputs.resourceId }
```
### Today's working options (all unsatisfying)
- Hand-written positional output that the producer must keep in lockstep with the source-array order:
```bicep
output dnsZoneIds object = {
blob: dnsZones[0].outputs.resourceId
servicebus: dnsZones[1].outputs.resourceId
}
```
- Synthesize IDs with `resourceId()` (no module indexing, so BCP247 does not fire), but this loses the implicit graph dependency on the module collection and bakes in scope assumptions:
```bicep
output dnsZoneIds object = toObject(
zones,
z => z.key,
z => resourceId(subscription().subscriptionId, resourceGroup().name, 'Microsoft.Network/privateDnsZones', z.name)
)
```
- Declare one module per item: verbose and defeats the point of the loop.
### Why this matters:
"Fan out a set of resources and expose them by name" is a very common pattern (DNS zones, role assignments, KV secrets, storage containers, etc.). Consumers should be insulated from positional ordering inside the producer, but the language currently forces the producer to either:
- (a) hand-maintain a parallel positional literal
- (b) drop the module dependency and rebuild IDs from data
- (c) fan out by hand.
### Possible directions (any one would solve it)
1. Relax BCP247 to allow lambda variables inside module/resource collection indexing when the index expression resolves to a compile-time constant per iteration (e.g. `dnsZones[indexOf(keys, k)]`).
2. Object comprehensions, e.g. `{ for z in zones: z.key => dnsZones[indexOf(keys, z.key)].outputs.resourceId }`.
3. Keyed module/resource loops: declare loops with object iteration so instances are addressable by key (`dnsZones['blob'].outputs.resourceId`).
4. Allow `toObject` value selectors to reference loop-collection items by lambda-derived index when symbol resolution can be proven constant.
Curious which direction the team would consider most tractable, and whether there is a better idiom we are missing today.
### Environment
- Bicep CLI version 0.43.8 (310735909d)
Contributor guide
Research direction
No implementation files or tests are named. Start by examining the BCP247 handling and the behavior of toObject and object outputs, then compare the four proposed language directions and establish which semantics the compiler can support. Done means a chosen approach is specified, implemented, and covered by compiler tests for keyed module-loop outputs.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- azure
- Domain
- cloud, compilers
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 30/100