-
Notifications
You must be signed in to change notification settings - Fork 340
Description
Summary
The dotnet generator needs to update the code it produces for PropagateGet methods on models that have array/collection properties backed by CLR types (e.g., IList<T>, IReadOnlyList<T>, arrays). Currently the generated PropagateGet only handles indexed paths (e.g., $.properties.virtualMachines[0].id) but returns false for non-indexed array-level paths (e.g., $.properties.virtualMachines). This prevents JsonPatch.EnumerateArray from working on CLR-backed collections.
Context
PR Azure/azure-sdk-for-net#56826 added the JsonPatch.EnumerateArray API to System.ClientModel. This API iterates over JSON array elements at a given path, yielding each element as ReadOnlyMemory<byte>. For this to work, the PropagateGet callback registered via JsonPatch.SetPropagators must be able to resolve array-level paths — not just indexed element paths.
That PR includes a hand-written AvailabilitySetDataV2 test model demonstrating the required "v2" pattern alongside the existing "v1" AvailabilitySetData model.
What changed: V1 → V2 PropagateGet
The only behavioral difference between V1 and V2 is in PropagateGet. Everything else (PropagateSet, Deserialize, Serialize structure, constructor, properties) is identical.
V1 (current generator output)
private bool PropagateGet(ReadOnlySpan<byte> jsonPath, out JsonPatch.EncodedValue value)
{
ReadOnlySpan<byte> local = jsonPath.SliceToStartOfPropertyName();
value = default;
// ... other property branches ...
else if (local.StartsWith("properties.virtualMachines"u8))
{
int propertyLength = "properties.virtualMachines"u8.Length;
ReadOnlySpan<byte> indexSlice = local.Slice(propertyLength);
// V1: only handles indexed paths — falls through to TryGetIndex
// which returns false when indexSlice is empty (no index)
if (!SerializationHelpers.TryGetIndex(indexSlice, out int index, out int bytesConsumed))
return false;
if (VirtualMachines.Count > index)
return VirtualMachines[index].Patch.TryGetEncodedValue(
[.. "$"u8, .. indexSlice.Slice(bytesConsumed + 2)], out value);
}
return false;
}V2 (what the generator should produce)
private bool PropagateGet(ReadOnlySpan<byte> jsonPath, out JsonPatch.EncodedValue value)
{
ReadOnlySpan<byte> local = jsonPath.SliceToStartOfPropertyName();
value = default;
// ... other property branches ...
else if (local.StartsWith("properties.virtualMachines"u8))
{
int propertyLength = "properties.virtualMachines"u8.Length;
ReadOnlySpan<byte> indexSlice = local.Slice(propertyLength);
// V2 ENHANCEMENT: handle array-level path (no index) by serializing CLR collection
if (indexSlice.IsEmpty)
{
return TryResolveVirtualMachinesArray(out value);
}
if (!SerializationHelpers.TryGetIndex(indexSlice, out int index, out int bytesConsumed))
return false;
if (VirtualMachines.Count > index)
return VirtualMachines[index].Patch.TryGetEncodedValue(
[.. "$"u8, .. indexSlice.Slice(bytesConsumed + 2)], out value);
}
return false;
}Plus these supporting methods per array property:
private bool TryResolveVirtualMachinesArray(out JsonPatch.EncodedValue value)
{
value = default;
BinaryData data = ModelReaderWriter.Write(
ActiveVirtualMachines(), new ModelReaderWriterOptions("J"));
var tempPatch = new JsonPatch();
tempPatch.Set("$"u8, data.ToMemory().Span);
return tempPatch.TryGetEncodedValue("$"u8, out value);
}
private IEnumerable<WritableSubResource> ActiveVirtualMachines()
{
if (!OptionalProperty.IsCollectionDefined(VirtualMachines))
yield break;
for (int i = 0; i < VirtualMachines.Count; i++)
{
if (!VirtualMachines[i].Patch.IsRemoved("$"u8))
yield return VirtualMachines[i];
}
}Why this matters
Without the V2 pattern, JsonPatch.EnumerateArray("$.properties.virtualMachines"u8) cannot resolve the array from CLR data when no patch-level replacement exists at that path. The propagator returns false, so EnumerateArray has no array data to iterate over.
What the generator needs to do
For every collection/array property that has a PropagateGet branch:
- Add an
indexSlice.IsEmptycheck before theTryGetIndexcall - Generate a
TryResolve{PropertyName}Arraymethod that serializes the active (non-removed) CLR items viaModelReaderWriter.Writeand returns the result as anEncodedValue - Generate an
Active{PropertyName}iterator that filters out removed items
Reference files
- V1 model:
AvailabilitySetData.cs/.Serialization.cs - V2 model:
AvailabilitySetDataV2.cs/.Serialization.cs - PR: Azure/azure-sdk-for-net#56826