From 8c5bf40fcbc81f9b833c06a1602794eb7f9b254c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:07:51 +0000 Subject: [PATCH 1/3] Initial plan From cae93e2e85bc07d5d8c3febb69f6011f080ed9b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:46:35 +0000 Subject: [PATCH 2/3] feat: emit V2 PropagateGet pattern for JsonPatch.EnumerateArray support on CLR arrays Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- .../MrwSerializationTypeDefinition.Dynamic.cs | 139 +++++++++++++++++- .../MrwSerializationTypeDefinition.cs | 6 + .../DynamicModelSerializationTests.cs | 35 +++++ .../PropagateCustomizedDynamicListProperty.cs | 4 + ...agateCustomizedDynamicListPropertyMixed.cs | 4 + .../PropagateModelListProperty.cs | 4 + ...PropagateModelListPropertyHelperMethods.cs | 42 ++++++ .../PropagateMultipleDynamicProperties.cs | 8 + .../Models/DynamicModel.Serialization.cs | 37 +++++ 9 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/PropagateModelListPropertyHelperMethods.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.Dynamic.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.Dynamic.cs index ecffc906348..4ea34b39739 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.Dynamic.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.Dynamic.cs @@ -382,7 +382,7 @@ private MethodBodyStatement[] BuildDynamicPropertyIfStatements( return [.. statements]; } - private static MethodBodyStatement[] BuildCollectionIfStatements( + private MethodBodyStatement[] BuildCollectionIfStatements( PropertyProvider property, bool propagateGet, ParameterProvider valueParameter, @@ -392,6 +392,7 @@ private static MethodBodyStatement[] BuildCollectionIfStatements( var currentType = property.Type; var accessorChain = new List { new IndexableExpression(property) }; ValueExpression? remainderSlice = null; + bool isFirstLevel = true; statements.Add(Declare("propertyLength", typeof(int), LiteralU8(GetJsonSerializedName(property.WireInfo!)).Property("Length"), @@ -407,6 +408,20 @@ private static MethodBodyStatement[] BuildCollectionIfStatements( if (currentType.IsList || currentType.IsArray) { + // V2: when PropagateGet and the outermost element is a direct dynamic model, + // add an IsEmpty check so that array-level paths (e.g. "$.properties.items") + // can be resolved via serialization of the CLR collection. + if (propagateGet && isFirstLevel && IsDirectDynamicListProperty(property)) + { + string tryResolveMethodName = $"TryResolve{property.Name}Array"; + VariableExpression valueVar = valueParameter; + statements.Add(new IfStatement(currentSlice.Property("IsEmpty")) + { + Return(new InvokeMethodExpression(null, tryResolveMethodName, + [new VariableExpression(valueVar.Type, valueVar.Declaration, IsOut: true)])) + }); + } + statements.Add(new IfStatement(Not(currentSlice.Invoke( "TryGetIndex", [ @@ -457,6 +472,7 @@ private static MethodBodyStatement[] BuildCollectionIfStatements( } currentType = currentType.ElementType; + isFirstLevel = false; } var finalAccessor = accessorChain.Last(); @@ -481,6 +497,127 @@ private static MethodBodyStatement[] BuildCollectionIfStatements( return [.. statements]; } + + /// + /// Returns true if the property is a list or array whose direct element type is a dynamic model with a Patch property. + /// Only such properties support the V2 IsEmpty / TryResolveArray pattern. + /// + private static bool IsDirectDynamicListProperty(PropertyProvider property) + { + if (!property.Type.IsList && !property.Type.IsArray) + return false; + return ScmCodeModelGenerator.Instance.TypeFactory.CSharpTypeMap.TryGetValue( + property.Type.ElementType, + out var provider) && + provider is ScmModelProvider { JsonPatchProperty: not null }; + } + + /// + /// Builds the TryResolve{PropertyName}Array helper method that serializes the active + /// (non-removed) CLR items as a JSON array and returns an . + /// + private MethodProvider BuildTryResolveArrayMethod(PropertyProvider property) + { + var valueParameter = new ParameterProvider("value", $"", typeof(JsonPatch.EncodedValue), isOut: true); + + var signature = new MethodSignature( + $"TryResolve{property.Name}Array", + $"", + MethodSignatureModifiers.Private, + typeof(bool), + $"", + [valueParameter]); + + var dataDeclStatement = Declare("data", typeof(BinaryData), + Static(typeof(ModelReaderWriter)).Invoke(nameof(ModelReaderWriter.Write), [ + new InvokeMethodExpression(null, $"Active{property.Name}", []), + New.Instance(typeof(ModelReaderWriterOptions), Literal("J")) + ]), + out var dataVar); + + var tempPatchDeclStatement = Declare("tempPatch", typeof(JsonPatch), New.Instance(typeof(JsonPatch)), out var tempPatchVar); + + var bodyStatements = new MethodBodyStatement[] + { + valueParameter.Assign(Default).Terminate(), + dataDeclStatement, + tempPatchDeclStatement, + tempPatchVar.As().Set(LiteralU8("$"), dataVar.Invoke("ToMemory").Property("Span")), + Return(tempPatchVar.As().TryGetEncodedValue(LiteralU8("$"), valueParameter)) + }; + + return new MethodProvider( + signature, + bodyStatements, + _model, + suppressions: [ScmModelProvider.JsonPatchSuppression]); + } + + /// + /// Builds the Active{PropertyName} iterator that yields non-removed items from the CLR collection. + /// + private MethodProvider BuildActiveItemsMethod(PropertyProvider property) + { + var elementType = property.Type.ElementType; + var returnType = new CSharpType(typeof(IEnumerable<>), elementType); + + var signature = new MethodSignature( + $"Active{property.Name}", + $"", + MethodSignatureModifiers.Private, + returnType, + $"", + []); + + var propertyExpr = new IndexableExpression(property); + string lengthPropertyName = property.Type.IsArray ? "Length" : "Count"; + + var indexDeclaration = Declare("i", out var indexVar); + var forStatement = new ForStatement( + indexDeclaration.Assign(Literal(0)), + indexVar.LessThan(((ValueExpression)property).Property(lengthPropertyName)), + indexVar.Increment()) + { + new IfStatement(Not(propertyExpr[indexVar].Property("Patch").As().IsRemoved(LiteralU8("$")))) + { + YieldReturn(propertyExpr[indexVar]) + } + }; + + var bodyStatements = new MethodBodyStatement[] + { + new IfStatement(Not(OptionalSnippets.IsCollectionDefined((ValueExpression)property))) + { + YieldBreak() + }, + forStatement + }; + + return new MethodProvider( + signature, + bodyStatements, + _model, + suppressions: [ScmModelProvider.JsonPatchSuppression]); + } + + /// + /// Returns all collection properties from this model and its base models that qualify for the + /// V2 PropagateGet pattern (outermost element is a direct dynamic model with a Patch property). + /// + private List GetQualifyingDynamicListProperties() + { + var result = new List(); + var currentModel = _model; + while (currentModel != null) + { + var qualifyingProps = currentModel.CanonicalView.Properties + .Where(p => p.WireInfo != null && IsDirectDynamicListProperty(p)); + result.AddRange(qualifyingProps); + currentModel = currentModel.BaseModelProvider as ScmModelProvider; + } + return result; + } + #pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. private static string BuildJsonPathForElement(string propertySerializedName, List indices) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs index 0cecfe33b29..92def9960df 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/MrwSerializationTypeDefinition.cs @@ -244,6 +244,12 @@ protected override MethodProvider[] BuildMethods() if (_model is ScmModelProvider { IsDynamicModel: true, HasDynamicProperties: true }) { methods.AddRange(BuildPropagateGetMethod(), BuildPropagateSetMethod()); + + // Add V2 helper methods for every qualifying list/array property + foreach (var prop in GetQualifyingDynamicListProperties()) + { + methods.AddRange(BuildTryResolveArrayMethod(prop), BuildActiveItemsMethod(prop)); + } } return [.. methods]; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/DynamicModelSerializationTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/DynamicModelSerializationTests.cs index a06cbeb18ea..f8e3b191180 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/DynamicModelSerializationTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/DynamicModelSerializationTests.cs @@ -357,6 +357,41 @@ public void PropagateModelListProperty() Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } + [Test] + public void PropagateModelListPropertyHelperMethods() + { + var inputModel = InputFactory.Model( + "dynamicModel", + isDynamicModel: true, + properties: + [ + InputFactory.Property("p1", + InputFactory.Array(InputFactory.Model( + "anotherDynamic", + isDynamicModel: true, + properties: + [ + InputFactory.Property("a1", InputPrimitiveType.String, isRequired: true) + ]))) + ]); + + MockHelpers.LoadMockGenerator(inputModels: () => [inputModel]); + var model = ScmCodeModelGenerator.Instance.TypeFactory.CreateModel(inputModel) as ClientModel.Providers.ScmModelProvider; + + Assert.IsNotNull(model); + Assert.IsTrue(model!.IsDynamicModel); + var serialization = model.SerializationProviders.SingleOrDefault(); + Assert.IsNotNull(serialization); + + var writer = new TypeProviderWriter(new FilteredMethodsTypeProvider( + serialization!, + name => name is "TryResolveP1Array" or "ActiveP1")); + + var file = writer.Write(); + + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + [Test] public void PropagateModelDictionaryProperty() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/PropagateCustomizedDynamicListProperty.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/PropagateCustomizedDynamicListProperty.cs index 171b6265d82..df04101b008 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/PropagateCustomizedDynamicListProperty.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/PropagateCustomizedDynamicListProperty.cs @@ -19,6 +19,10 @@ private bool PropagateGet(global::System.ReadOnlySpan jsonPath, out global { int propertyLength = "prop1"u8.Length; global::System.ReadOnlySpan currentSlice = local.Slice(propertyLength); + if (currentSlice.IsEmpty) + { + return TryResolveProp2Array(out value); + } if (!currentSlice.TryGetIndex(out int index, out int bytesConsumed)) { return false; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/PropagateCustomizedDynamicListPropertyMixed.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/PropagateCustomizedDynamicListPropertyMixed.cs index 171b6265d82..df04101b008 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/PropagateCustomizedDynamicListPropertyMixed.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/PropagateCustomizedDynamicListPropertyMixed.cs @@ -19,6 +19,10 @@ private bool PropagateGet(global::System.ReadOnlySpan jsonPath, out global { int propertyLength = "prop1"u8.Length; global::System.ReadOnlySpan currentSlice = local.Slice(propertyLength); + if (currentSlice.IsEmpty) + { + return TryResolveProp2Array(out value); + } if (!currentSlice.TryGetIndex(out int index, out int bytesConsumed)) { return false; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/PropagateModelListProperty.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/PropagateModelListProperty.cs index 316082a80e2..a506a0e6f4a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/PropagateModelListProperty.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/PropagateModelListProperty.cs @@ -19,6 +19,10 @@ private bool PropagateGet(global::System.ReadOnlySpan jsonPath, out global { int propertyLength = "p1"u8.Length; global::System.ReadOnlySpan currentSlice = local.Slice(propertyLength); + if (currentSlice.IsEmpty) + { + return TryResolveP1Array(out value); + } if (!currentSlice.TryGetIndex(out int index, out int bytesConsumed)) { return false; diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/PropagateModelListPropertyHelperMethods.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/PropagateModelListPropertyHelperMethods.cs new file mode 100644 index 00000000000..be82087a935 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/PropagateModelListPropertyHelperMethods.cs @@ -0,0 +1,42 @@ +// + +#nullable disable + +using System; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using Sample.Models; + +namespace Sample +{ + public partial class DynamicModel + { +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + private bool TryResolveP1Array(out global::System.ClientModel.Primitives.JsonPatch.EncodedValue value) + { + value = default; + global::System.BinaryData data = global::System.ClientModel.Primitives.ModelReaderWriter.Write(ActiveP1(), new global::System.ClientModel.Primitives.ModelReaderWriterOptions("J")); + global::System.ClientModel.Primitives.JsonPatch tempPatch = new global::System.ClientModel.Primitives.JsonPatch(); + tempPatch.Set("$"u8, data.ToMemory().Span); + return tempPatch.TryGetEncodedValue("$"u8, out value); + } +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + private global::System.Collections.Generic.IEnumerable ActiveP1() + { + if (!global::Sample.Optional.IsCollectionDefined(P1)) + { + yield break; + } + for (int i = 0; (i < P1.Count); i++) + { + if (!P1[i].Patch.IsRemoved("$"u8)) + { + yield return P1[i]; + } + } + } +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/PropagateMultipleDynamicProperties.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/PropagateMultipleDynamicProperties.cs index fdfd6a8104c..7aac9e57f7a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/PropagateMultipleDynamicProperties.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/PropagateMultipleDynamicProperties.cs @@ -32,6 +32,10 @@ private bool PropagateGet(global::System.ReadOnlySpan jsonPath, out global { int propertyLength = "p2"u8.Length; global::System.ReadOnlySpan currentSlice = local.Slice(propertyLength); + if (currentSlice.IsEmpty) + { + return TryResolveP2Array(out value); + } if (!currentSlice.TryGetIndex(out int index, out int bytesConsumed)) { return false; @@ -42,6 +46,10 @@ private bool PropagateGet(global::System.ReadOnlySpan jsonPath, out global { int propertyLength = "p3"u8.Length; global::System.ReadOnlySpan currentSlice = local.Slice(propertyLength); + if (currentSlice.IsEmpty) + { + return TryResolveP3Array(out value); + } if (!currentSlice.TryGetIndex(out int index, out int bytesConsumed)) { return false; diff --git a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/DynamicModel.Serialization.cs b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/DynamicModel.Serialization.cs index 804c2d3a362..98b28e537ef 100644 --- a/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/DynamicModel.Serialization.cs +++ b/packages/http-client-csharp/generator/TestProjects/Local/Sample-TypeSpec/src/Generated/Models/DynamicModel.Serialization.cs @@ -773,6 +773,10 @@ private bool PropagateGet(ReadOnlySpan jsonPath, out JsonPatch.EncodedValu { int propertyLength = "listFoo"u8.Length; ReadOnlySpan currentSlice = local.Slice(propertyLength); + if (currentSlice.IsEmpty) + { + return TryResolveListFooArray(out value); + } if (!currentSlice.TryGetIndex(out int index, out int bytesConsumed)) { return false; @@ -966,5 +970,38 @@ private bool PropagateSet(ReadOnlySpan jsonPath, JsonPatch.EncodedValue va return false; } #pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + + /// + /// + /// +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + 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); + } +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + + /// + /// +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + private IEnumerable 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]; + } + } + } +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. } } From b671a9e5272c447e5067d182a9504fef290e8715 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 20:49:21 +0000 Subject: [PATCH 3/3] chore: add changeset for V2 PropagateGet pattern Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- ...ilot-update-propagateget-pattern-2026-03-06-20-49-06.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/copilot-update-propagateget-pattern-2026-03-06-20-49-06.md diff --git a/.chronus/changes/copilot-update-propagateget-pattern-2026-03-06-20-49-06.md b/.chronus/changes/copilot-update-propagateget-pattern-2026-03-06-20-49-06.md new file mode 100644 index 00000000000..f267f80a6cb --- /dev/null +++ b/.chronus/changes/copilot-update-propagateget-pattern-2026-03-06-20-49-06.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/http-client-csharp" +--- + +Generator now emits V2 `PropagateGet` pattern for list/array properties backed by CLR types: adds an `IsEmpty` check at the outermost array level and generates `TryResolve{PropertyName}Array` / `Active{PropertyName}` helpers to support `JsonPatch.EnumerateArray` on CLR-backed collections.