From 2d45922f6d82e31456ba65e043948db031fa8f29 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:00:12 +0000 Subject: [PATCH 1/4] Initial plan From 45030c7bbbbe113134ac713c5e8a73efbedd4bce Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Mar 2026 22:28:58 +0000 Subject: [PATCH 2/4] fix: generate JsonPatch for dynamic models with Record (BinaryData additional properties) Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- .../src/Providers/ScmModelProvider.cs | 35 ++++++++++++++++--- .../ScmModelProvider/ScmModelProviderTests.cs | 5 +++ ...namicModelWithBinaryDataAdditionalProps.cs | 23 +++++++----- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmModelProvider.cs index 978bc7ad993..5a3abf57061 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmModelProvider.cs @@ -78,6 +78,13 @@ protected override PropertyProvider[] BuildProperties() return base.BuildProperties(); } + // For dynamic models with BinaryData additional properties (Record), + // skip generating AdditionalProperties since JsonPatch handles dynamic properties. + if (SupportsBinaryDataAdditionalProperties) + { + return [JsonPatchProperty, .. base.BuildProperties().Where(p => !p.IsAdditionalProperties)]; + } + return [JsonPatchProperty, .. base.BuildProperties()]; } @@ -122,7 +129,8 @@ protected override ConstructorProvider[] BuildConstructors() foreach (var statement in constructor.BodyStatements) { if (statement is ExpressionStatement { Expression: AssignmentExpression assignmentExpression } - && assignmentExpression.Value.Equals(RawDataField.AsParameter)) + && (assignmentExpression.Value.Equals(RawDataField.AsParameter) || + assignmentExpression.Variable is MemberExpression { MemberName: AdditionalPropertiesHelper.AdditionalBinaryDataPropsFieldName })) { continue; } @@ -152,6 +160,20 @@ protected override ConstructorProvider[] BuildConstructors() constructor.Update(bodyStatements: updatedBody); } } + else if (JsonPatchField != null && SupportsBinaryDataAdditionalProperties && constructor.BodyStatements != null) + { + // Remove the additional binary data properties initialization from the init constructor + var updatedBody = constructor.BodyStatements + .Where(s => s is not ExpressionStatement + { + Expression: AssignmentExpression + { + Variable: MemberExpression { MemberName: AdditionalPropertiesHelper.AdditionalBinaryDataPropsFieldName } + } + }) + .ToList(); + constructor.Update(bodyStatements: updatedBody); + } updatedConstructors.Add(constructor); } @@ -166,7 +188,7 @@ protected override ConstructorProvider[] BuildConstructors() private FieldProvider? BuildJsonPatchField() { - if (!IsDynamicModel || SupportsBinaryDataAdditionalProperties) + if (!IsDynamicModel) { return null; } @@ -232,10 +254,13 @@ private bool ShouldUpdateFullConstructor() return true; } - return FullConstructor.Signature.Parameters - .Any(p => p.Field?.Name.Equals(AdditionalPropertiesHelper.AdditionalBinaryDataPropsFieldName) == true); + return FullConstructor.Signature.Parameters.Any(IsAdditionalBinaryDataParameter); } + private static bool IsAdditionalBinaryDataParameter(ParameterProvider p) => + p.Field?.Name.Equals(AdditionalPropertiesHelper.AdditionalBinaryDataPropsFieldName) == true || + p.Property?.BackingField?.Name.Equals(AdditionalPropertiesHelper.AdditionalBinaryDataPropsFieldName) == true; + private void UpdateFullConstructorParameters() { if (BaseJsonPatchProperty.Value is null) @@ -249,7 +274,7 @@ private void UpdateFullConstructorParameters() foreach (var parameter in FullConstructor.Signature.Parameters) { - if (parameter.Field?.Name.Equals(AdditionalPropertiesHelper.AdditionalBinaryDataPropsFieldName) == true) + if (IsAdditionalBinaryDataParameter(parameter)) { updatedParameters.Add(jsonPatchParameter); } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/ScmModelProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/ScmModelProviderTests.cs index a3879c800d2..1d95daa5b0a 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/ScmModelProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/ScmModelProviderTests.cs @@ -157,6 +157,11 @@ public void TestDynamicModelWithBinaryDataAdditionalProps() Assert.IsNotNull(model); Assert.IsTrue(model!.IsDynamicModel); + // Dynamic models with Record (BinaryData additional properties) should generate + // JsonPatch instead of AdditionalProperties. + Assert.IsNotNull(model.JsonPatchProperty, "Dynamic models with Record should generate JsonPatch"); + Assert.IsFalse(model.Properties.Any(p => p.IsAdditionalProperties), + "Dynamic models with Record should not generate AdditionalProperties"); AssertJsonIgnoreAttributeOnPatchProperty(model); var writer = new TypeProviderWriter(model); diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/TestData/ScmModelProviderTests/TestDynamicModelWithBinaryDataAdditionalProps.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/TestData/ScmModelProviderTests/TestDynamicModelWithBinaryDataAdditionalProps.cs index 53c73900574..0a4f0f7e108 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/TestData/ScmModelProviderTests/TestDynamicModelWithBinaryDataAdditionalProps.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/TestData/ScmModelProviderTests/TestDynamicModelWithBinaryDataAdditionalProps.cs @@ -2,32 +2,39 @@ #nullable disable -using System; -using System.Collections.Generic; +using System.ClientModel.Primitives; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; using Sample; namespace Sample.Models { public partial class DynamicModel { - private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties; + [global::System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SCME0001")] + private global::System.ClientModel.Primitives.JsonPatch _patch; public DynamicModel(string p1) { global::Sample.Argument.AssertNotNull(p1, nameof(p1)); P1 = p1; - _additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); } - internal DynamicModel(string p1, global::System.Collections.Generic.IDictionary additionalProperties) +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + internal DynamicModel(string p1, in global::System.ClientModel.Primitives.JsonPatch patch) { P1 = p1; - _additionalBinaryDataProperties = additionalProperties; + _patch = patch; } +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. - public string P1 { get; set; } + [global::System.Text.Json.Serialization.JsonIgnoreAttribute] + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SCME0001")] + public ref global::System.ClientModel.Primitives.JsonPatch Patch => ref _patch; - public global::System.Collections.Generic.IDictionary AdditionalProperties => _additionalBinaryDataProperties; + public string P1 { get; set; } } } From efb10634ced3ca9460622baa001af4a288fc7b4b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 20:16:33 +0000 Subject: [PATCH 3/4] fix: add backcompat support when @dynamicModel is added to a model that was previously shipped with AdditionalProperties Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- .../src/Providers/ScmModelProvider.cs | 42 ++++++++++++++--- .../ScmModelProvider/ScmModelProviderTests.cs | 34 ++++++++++++++ ...WithBinaryDataAdditionalPropsBackCompat.cs | 47 +++++++++++++++++++ .../DynamicModel.cs | 14 ++++++ 4 files changed, 131 insertions(+), 6 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/TestData/ScmModelProviderTests/TestDynamicModelWithBinaryDataAdditionalPropsBackCompat.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/TestData/ScmModelProviderTests/TestDynamicModelWithBinaryDataAdditionalPropsBackCompat/DynamicModel.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmModelProvider.cs index 5a3abf57061..567d473b7c6 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmModelProvider.cs @@ -39,6 +39,11 @@ public sealed class ScmModelProvider : ModelProvider internal bool HasDynamicProperties => _hasDynamicProperties ??= BuildHasDynamicProperties(); private bool? _hasDynamicProperties; + // When true, the model needs to generate both JsonPatch and AdditionalProperties for + // backward compatibility (the model was previously shipped with AdditionalProperties). + private bool NeedsBackCompatAdditionalProperties => _needsBackCompatAdditionalProperties ??= BuildNeedsBackCompatAdditionalProperties(); + private bool? _needsBackCompatAdditionalProperties; + internal static SuppressionStatement JsonPatchSuppression = new SuppressionStatement(null, Literal(ScmEvaluationTypeDiagnosticId), ScmEvaluationTypeSuppressionJustification); @@ -62,10 +67,12 @@ protected override FieldProvider[] BuildFields() foreach (var field in fields) { - if (!field.Equals(RawDataField)) + // Keep the RawDataField when backcompat requires AdditionalProperties to be generated alongside JsonPatch + if (field.Equals(RawDataField) && !NeedsBackCompatAdditionalProperties) { - updatedFields.Add(field); + continue; } + updatedFields.Add(field); } return [JsonPatchField, .. updatedFields]; @@ -80,7 +87,8 @@ protected override PropertyProvider[] BuildProperties() // For dynamic models with BinaryData additional properties (Record), // skip generating AdditionalProperties since JsonPatch handles dynamic properties. - if (SupportsBinaryDataAdditionalProperties) + // Exception: when backcompat requires preserving AdditionalProperties from the last contract. + if (SupportsBinaryDataAdditionalProperties && !NeedsBackCompatAdditionalProperties) { return [JsonPatchProperty, .. base.BuildProperties().Where(p => !p.IsAdditionalProperties)]; } @@ -130,7 +138,8 @@ protected override ConstructorProvider[] BuildConstructors() { if (statement is ExpressionStatement { Expression: AssignmentExpression assignmentExpression } && (assignmentExpression.Value.Equals(RawDataField.AsParameter) || - assignmentExpression.Variable is MemberExpression { MemberName: AdditionalPropertiesHelper.AdditionalBinaryDataPropsFieldName })) + (!NeedsBackCompatAdditionalProperties && + assignmentExpression.Variable is MemberExpression { MemberName: AdditionalPropertiesHelper.AdditionalBinaryDataPropsFieldName }))) { continue; } @@ -160,7 +169,7 @@ protected override ConstructorProvider[] BuildConstructors() constructor.Update(bodyStatements: updatedBody); } } - else if (JsonPatchField != null && SupportsBinaryDataAdditionalProperties && constructor.BodyStatements != null) + else if (JsonPatchField != null && SupportsBinaryDataAdditionalProperties && !NeedsBackCompatAdditionalProperties && constructor.BodyStatements != null) { // Remove the additional binary data properties initialization from the init constructor var updatedBody = constructor.BodyStatements @@ -276,7 +285,17 @@ private void UpdateFullConstructorParameters() { if (IsAdditionalBinaryDataParameter(parameter)) { - updatedParameters.Add(jsonPatchParameter); + if (NeedsBackCompatAdditionalProperties) + { + // Backcompat: keep the additionalBinaryData parameter and add patch after it + updatedParameters.Add(parameter); + updatedParameters.Add(jsonPatchParameter); + } + else + { + // Replace the additionalBinaryData parameter with patch + updatedParameters.Add(jsonPatchParameter); + } } else { @@ -367,5 +386,16 @@ private bool BuildHasDynamicProperties() return null; } + + private bool BuildNeedsBackCompatAdditionalProperties() + { + if (!IsDynamicModel || !SupportsBinaryDataAdditionalProperties || LastContractView == null) + { + return false; + } + + return LastContractView.Properties.Any(p => + p.Name == AdditionalPropertiesHelper.DefaultAdditionalPropertiesPropertyName); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/ScmModelProviderTests.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/ScmModelProviderTests.cs index 1d95daa5b0a..9dfe03131ff 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/ScmModelProviderTests.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/ScmModelProviderTests.cs @@ -170,6 +170,40 @@ public void TestDynamicModelWithBinaryDataAdditionalProps() Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } + [Test] + public async Task TestDynamicModelWithBinaryDataAdditionalPropsBackCompat() + { + // Scenario: A model was previously shipped with AdditionalProperties (IDictionary) + // but the model has now been updated to use @dynamicModel. Both JsonPatch and AdditionalProperties + // should be generated to maintain backward compatibility. + var inputModel = InputFactory.Model( + "dynamicModel", + isDynamicModel: true, + additionalProperties: InputPrimitiveType.Any, + properties: + [ + InputFactory.Property("p1", InputPrimitiveType.String, isRequired: true) + ]); + + await MockHelpers.LoadMockGeneratorAsync( + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(), + inputModels: () => [inputModel]); + var model = ScmCodeModelGenerator.Instance.TypeFactory.CreateModel(inputModel) as ScmModel; + + Assert.IsNotNull(model); + Assert.IsTrue(model!.IsDynamicModel); + // Backcompat: both JsonPatch and AdditionalProperties should be generated + Assert.IsNotNull(model.JsonPatchProperty, "Dynamic model should generate JsonPatch"); + Assert.IsTrue(model.Properties.Any(p => p.IsAdditionalProperties), + "Dynamic model should still generate AdditionalProperties for backcompat"); + AssertJsonIgnoreAttributeOnPatchProperty(model); + + var writer = new TypeProviderWriter(model); + var file = writer.Write(); + + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + [Test] public void TestDynamicModelWithUnionAdditionalProps() { diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/TestData/ScmModelProviderTests/TestDynamicModelWithBinaryDataAdditionalPropsBackCompat.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/TestData/ScmModelProviderTests/TestDynamicModelWithBinaryDataAdditionalPropsBackCompat.cs new file mode 100644 index 00000000000..93e3f01f540 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/TestData/ScmModelProviderTests/TestDynamicModelWithBinaryDataAdditionalPropsBackCompat.cs @@ -0,0 +1,47 @@ +// + +#nullable disable + +using System; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; +using Sample; + +namespace Sample.Models +{ + public partial class DynamicModel + { + [global::System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SCME0001")] + private global::System.ClientModel.Primitives.JsonPatch _patch; + private protected readonly global::System.Collections.Generic.IDictionary _additionalBinaryDataProperties; + + public DynamicModel(string p1) + { + global::Sample.Argument.AssertNotNull(p1, nameof(p1)); + + P1 = p1; + _additionalBinaryDataProperties = new global::Sample.ChangeTrackingDictionary(); + } + +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + internal DynamicModel(string p1, global::System.Collections.Generic.IDictionary additionalProperties, in global::System.ClientModel.Primitives.JsonPatch patch) + { + P1 = p1; + _additionalBinaryDataProperties = additionalProperties; + _patch = patch; + } +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + + [global::System.Text.Json.Serialization.JsonIgnoreAttribute] + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Never)] + [global::System.Diagnostics.CodeAnalysis.ExperimentalAttribute("SCME0001")] + public ref global::System.ClientModel.Primitives.JsonPatch Patch => ref _patch; + + public string P1 { get; set; } + + public global::System.Collections.Generic.IDictionary AdditionalProperties => _additionalBinaryDataProperties; + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/TestData/ScmModelProviderTests/TestDynamicModelWithBinaryDataAdditionalPropsBackCompat/DynamicModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/TestData/ScmModelProviderTests/TestDynamicModelWithBinaryDataAdditionalPropsBackCompat/DynamicModel.cs new file mode 100644 index 00000000000..faa54488597 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/ScmModelProvider/TestData/ScmModelProviderTests/TestDynamicModelWithBinaryDataAdditionalPropsBackCompat/DynamicModel.cs @@ -0,0 +1,14 @@ +// This file represents the previous (last contract) state of the model before @dynamicModel was added. +// It has AdditionalProperties but no JsonPatch, which triggers the backcompat path. + +using System.Collections.Generic; + +namespace Sample.Models +{ + public partial class DynamicModel + { + public string P1 { get; set; } + + public global::System.Collections.Generic.IDictionary AdditionalProperties { get; } + } +} From cee8e202b2a85c1bb76d36713b08a432e54ab1c7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:23:20 +0000 Subject: [PATCH 4/4] refactor: simplify UpdateFullConstructorParameters and add serialization backcompat tests Co-authored-by: JoshLove-msft <54595583+JoshLove-msft@users.noreply.github.com> --- .../src/Providers/ScmModelProvider.cs | 9 +-- .../DynamicModelSerializationTests.cs | 76 +++++++++++++++++++ ...WithBinaryDataAdditionalPropsBackCompat.cs | 38 ++++++++++ .../DynamicModel.cs | 14 ++++ ...WithBinaryDataAdditionalPropsBackCompat.cs | 61 +++++++++++++++ .../DynamicModel.cs | 14 ++++ 6 files changed, 205 insertions(+), 7 deletions(-) create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/DeserializeModelWithBinaryDataAdditionalPropsBackCompat.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/DeserializeModelWithBinaryDataAdditionalPropsBackCompat/DynamicModel.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/WriteModelWithBinaryDataAdditionalPropsBackCompat.cs create mode 100644 packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/WriteModelWithBinaryDataAdditionalPropsBackCompat/DynamicModel.cs diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmModelProvider.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmModelProvider.cs index 567d473b7c6..1faf1ae4044 100644 --- a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmModelProvider.cs +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/src/Providers/ScmModelProvider.cs @@ -287,15 +287,10 @@ private void UpdateFullConstructorParameters() { if (NeedsBackCompatAdditionalProperties) { - // Backcompat: keep the additionalBinaryData parameter and add patch after it + // Backcompat: keep the additionalBinaryData parameter updatedParameters.Add(parameter); - updatedParameters.Add(jsonPatchParameter); - } - else - { - // Replace the additionalBinaryData parameter with patch - updatedParameters.Add(jsonPatchParameter); } + updatedParameters.Add(jsonPatchParameter); } else { 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 8ee7575d29e..a06cbeb18ea 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 @@ -1489,5 +1489,81 @@ public void DeserializeDynamicDerivedModelWithNonDiscriminatedBase() var file = writer.Write(); Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); } + + [Test] + public async Task WriteModelWithBinaryDataAdditionalPropsBackCompat() + { + var inputModel = InputFactory.Model( + "dynamicModel", + isDynamicModel: true, + additionalProperties: InputPrimitiveType.Any, + properties: + [ + InputFactory.Property("p1", InputPrimitiveType.String, isRequired: true) + ]); + + await MockHelpers.LoadMockGeneratorAsync( + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(), + inputModels: () => [inputModel]); + var model = ScmCodeModelGenerator.Instance.TypeFactory.CreateModel(inputModel) as ClientModel.Providers.ScmModelProvider; + + Assert.IsNotNull(model); + Assert.IsTrue(model!.IsDynamicModel); + Assert.IsTrue(model.Properties.Any(p => p.IsAdditionalProperties), + "Backcompat model should still generate AdditionalProperties"); + + var serialization = model.SerializationProviders.SingleOrDefault(); + Assert.IsNotNull(serialization); + + // Ensure Constructors have been built (which updates FullConstructor parameters + // used by the serialization provider). In the full pipeline, the model's Constructors + // are always written before the serialization file. + Assert.AreEqual(2, model.Constructors.Count); + + var writer = new TypeProviderWriter(new FilteredMethodsTypeProvider( + serialization!, + name => name is "JsonModelWriteCore" or "Write")); + + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } + + [Test] + public async Task DeserializeModelWithBinaryDataAdditionalPropsBackCompat() + { + var inputModel = InputFactory.Model( + "dynamicModel", + isDynamicModel: true, + additionalProperties: InputPrimitiveType.Any, + properties: + [ + InputFactory.Property("p1", InputPrimitiveType.String, isRequired: true) + ]); + + await MockHelpers.LoadMockGeneratorAsync( + lastContractCompilation: async () => await Helpers.GetCompilationFromDirectoryAsync(), + inputModels: () => [inputModel]); + var model = ScmCodeModelGenerator.Instance.TypeFactory.CreateModel(inputModel) as ClientModel.Providers.ScmModelProvider; + + Assert.IsNotNull(model); + Assert.IsTrue(model!.IsDynamicModel); + Assert.IsTrue(model.Properties.Any(p => p.IsAdditionalProperties), + "Backcompat model should still generate AdditionalProperties"); + + var serialization = model.SerializationProviders.SingleOrDefault(); + Assert.IsNotNull(serialization); + + // Ensure Constructors have been built (which updates FullConstructor parameters + // used by the serialization provider). In the full pipeline, the model's Constructors + // are always written before the serialization file. + Assert.AreEqual(2, model.Constructors.Count); + + var writer = new TypeProviderWriter(new FilteredMethodsTypeProvider( + serialization!, + name => name.StartsWith("Deserialize"))); + + var file = writer.Write(); + Assert.AreEqual(Helpers.GetExpectedFromFile(), file.Content); + } } } diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/DeserializeModelWithBinaryDataAdditionalPropsBackCompat.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/DeserializeModelWithBinaryDataAdditionalPropsBackCompat.cs new file mode 100644 index 00000000000..f796749b994 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/DeserializeModelWithBinaryDataAdditionalPropsBackCompat.cs @@ -0,0 +1,38 @@ +// + +#nullable disable + +using System; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Text.Json; +using Sample.Models; + +namespace Sample +{ + public partial class DynamicModel + { + internal static global::Sample.Models.DynamicModel DeserializeDynamicModel(global::System.Text.Json.JsonElement element, global::System.BinaryData data, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + if ((element.ValueKind == global::System.Text.Json.JsonValueKind.Null)) + { + return null; + } + string p1 = default; + global::System.Collections.Generic.IDictionary additionalProperties = new global::Sample.ChangeTrackingDictionary(); +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + global::System.ClientModel.Primitives.JsonPatch patch = new global::System.ClientModel.Primitives.JsonPatch((data is null) ? global::System.ReadOnlyMemory.Empty : data.ToMemory()); +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + foreach (var prop in element.EnumerateObject()) + { + if (prop.NameEquals("p1"u8)) + { + p1 = prop.Value.GetString(); + continue; + } + additionalProperties.Add(prop.Name, global::System.BinaryData.FromString(prop.Value.GetRawText())); + } + return new global::Sample.Models.DynamicModel(p1, additionalProperties, patch); + } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/DeserializeModelWithBinaryDataAdditionalPropsBackCompat/DynamicModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/DeserializeModelWithBinaryDataAdditionalPropsBackCompat/DynamicModel.cs new file mode 100644 index 00000000000..faa54488597 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/DeserializeModelWithBinaryDataAdditionalPropsBackCompat/DynamicModel.cs @@ -0,0 +1,14 @@ +// This file represents the previous (last contract) state of the model before @dynamicModel was added. +// It has AdditionalProperties but no JsonPatch, which triggers the backcompat path. + +using System.Collections.Generic; + +namespace Sample.Models +{ + public partial class DynamicModel + { + public string P1 { get; set; } + + public global::System.Collections.Generic.IDictionary AdditionalProperties { get; } + } +} diff --git a/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/WriteModelWithBinaryDataAdditionalPropsBackCompat.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/WriteModelWithBinaryDataAdditionalPropsBackCompat.cs new file mode 100644 index 00000000000..53e20edaaa0 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/WriteModelWithBinaryDataAdditionalPropsBackCompat.cs @@ -0,0 +1,61 @@ +// + +#nullable disable + +using System; +using System.ClientModel.Primitives; +using System.Text.Json; +using Sample.Models; + +namespace Sample +{ + public partial class DynamicModel + { + global::System.BinaryData global::System.ClientModel.Primitives.IPersistableModel.Write(global::System.ClientModel.Primitives.ModelReaderWriterOptions options) => this.PersistableModelWriteCore(options); + + void global::System.ClientModel.Primitives.IJsonModel.Write(global::System.Text.Json.Utf8JsonWriter writer, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + if (Patch.Contains("$"u8)) + { + writer.WriteRawValue(Patch.GetJson("$"u8)); + return; + } +#pragma warning restore SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + + writer.WriteStartObject(); + this.JsonModelWriteCore(writer, options); + writer.WriteEndObject(); + } + + protected virtual void JsonModelWriteCore(global::System.Text.Json.Utf8JsonWriter writer, global::System.ClientModel.Primitives.ModelReaderWriterOptions options) + { + string format = (options.Format == "W") ? ((global::System.ClientModel.Primitives.IPersistableModel)this).GetFormatFromOptions(options) : options.Format; + if ((format != "J")) + { + throw new global::System.FormatException($"The model {nameof(global::Sample.Models.DynamicModel)} does not support writing '{format}' format."); + } +#pragma warning disable SCME0001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + if (!Patch.Contains("$.p1"u8)) + { + writer.WritePropertyName("p1"u8); + writer.WriteStringValue(P1); + } + foreach (var item in AdditionalProperties) + { + writer.WritePropertyName(item.Key); +#if NET6_0_OR_GREATER + writer.WriteRawValue(item.Value); +#else + using (global::System.Text.Json.JsonDocument document = global::System.Text.Json.JsonDocument.Parse(item.Value)) + { + global::System.Text.Json.JsonSerializer.Serialize(writer, document.RootElement); + } +#endif + } + + Patch.WriteTo(writer); +#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/WriteModelWithBinaryDataAdditionalPropsBackCompat/DynamicModel.cs b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/WriteModelWithBinaryDataAdditionalPropsBackCompat/DynamicModel.cs new file mode 100644 index 00000000000..faa54488597 --- /dev/null +++ b/packages/http-client-csharp/generator/Microsoft.TypeSpec.Generator.ClientModel/test/Providers/MrwSerializationTypeDefinitions/TestData/DynamicModelSerializationTests/WriteModelWithBinaryDataAdditionalPropsBackCompat/DynamicModel.cs @@ -0,0 +1,14 @@ +// This file represents the previous (last contract) state of the model before @dynamicModel was added. +// It has AdditionalProperties but no JsonPatch, which triggers the backcompat path. + +using System.Collections.Generic; + +namespace Sample.Models +{ + public partial class DynamicModel + { + public string P1 { get; set; } + + public global::System.Collections.Generic.IDictionary AdditionalProperties { get; } + } +}