Skip to content

[dotnet] Emit V2 PropagateGet pattern to support JsonPatch.EnumerateArray on CLR arrays#9957

Draft
Copilot wants to merge 3 commits intomainfrom
copilot/update-propagateget-pattern
Draft

[dotnet] Emit V2 PropagateGet pattern to support JsonPatch.EnumerateArray on CLR arrays#9957
Copilot wants to merge 3 commits intomainfrom
copilot/update-propagateget-pattern

Conversation

Copy link
Contributor

Copilot AI commented Mar 6, 2026

The generated PropagateGet method returned false for array-level JSON paths (e.g., $.properties.items) on CLR-backed list properties, making JsonPatch.EnumerateArray non-functional for those paths. Only indexed paths ($.properties.items[0]) were handled.

Changes

Generator logic (MrwSerializationTypeDefinition.Dynamic.cs)

  • Added IsEmpty check before the first TryGetIndex call in BuildCollectionIfStatements — only for outermost list/array properties whose direct element type is a dynamic model (nested collections like List<List<T>> are intentionally excluded since ModelReaderWriter.Write does not support IEnumerable<IList<T>>)
  • Added BuildTryResolveArrayMethod: generates TryResolve{PropertyName}Array(out EncodedValue) — serializes active CLR items via ModelReaderWriter.Write + a temporary JsonPatch
  • Added BuildActiveItemsMethod: generates Active{PropertyName}() — yields items where !item.Patch.IsRemoved("$"u8)
  • Added IsDirectDynamicListProperty / GetQualifyingDynamicListProperties helpers

Method registration (MrwSerializationTypeDefinition.cs)

  • Registers TryResolve{PropertyName}Array + Active{PropertyName} in BuildMethods() for each qualifying collection property

Generated output delta

Before (V1):

if (!currentSlice.TryGetIndex(out int index, out int bytesConsumed))
{
    return false;
}
return ListFoo[index].Patch.TryGetEncodedValue([.. "$"u8, .. currentSlice.Slice(bytesConsumed)], out value);

After (V2):

if (currentSlice.IsEmpty)
{
    return TryResolveListFooArray(out value);
}
if (!currentSlice.TryGetIndex(out int index, out int bytesConsumed))
{
    return false;
}
return ListFoo[index].Patch.TryGetEncodedValue([.. "$"u8, .. currentSlice.Slice(bytesConsumed)], out value);

// ...

private bool TryResolveListFooArray(out JsonPatch.EncodedValue value)
{
    value = default;
    BinaryData data = ModelReaderWriter.Write(ActiveListFoo(), new ModelReaderWriterOptions("J"));
    JsonPatch tempPatch = new JsonPatch();
    tempPatch.Set("$"u8, data.ToMemory().Span);
    return tempPatch.TryGetEncodedValue("$"u8, out value);
}

private IEnumerable<AnotherDynamicModel> ActiveListFoo()
{
    if (!Optional.IsCollectionDefined(ListFoo)) { yield break; }
    for (int i = 0; (i < ListFoo.Count); i++)
    {
        if (!ListFoo[i].Patch.IsRemoved("$"u8))
            yield return ListFoo[i];
    }
}
Original prompt

This section details on the original issue you should resolve

<issue_title>[dotnet] Generator should emit V2 PropagateGet pattern to support JsonPatch.EnumerateArray on CLR arrays</issue_title>
<issue_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:

  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...

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

…rt on CLR arrays

Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com>
@microsoft-github-policy-service microsoft-github-policy-service bot added the emitter:client:csharp Issue for the C# client emitter: @typespec/http-client-csharp label Mar 6, 2026
Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com>
Copilot AI changed the title [WIP] Update generator to support V2 PropagateGet pattern for JsonPatch [dotnet] Emit V2 PropagateGet pattern to support JsonPatch.EnumerateArray on CLR arrays Mar 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

2 participants