Skip to content

[dotnet] Generator should emit V2 PropagateGet pattern to support JsonPatch.EnumerateArray on CLR arrays #9956

@m-nash

Description

@m-nash

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:

  1. Add an indexSlice.IsEmpty check before the TryGetIndex call
  2. Generate a TryResolve{PropertyName}Array method that serializes the active (non-removed) CLR items via ModelReaderWriter.Write and returns the result as an EncodedValue
  3. Generate an Active{PropertyName} iterator that filters out removed items

Reference files

Metadata

Metadata

Labels

emitter:client:csharpIssue for the C# client emitter: @typespec/http-client-csharp

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions