diff --git a/eng/Packages.Data.props b/eng/Packages.Data.props
index 4588f3ab075c..735d09b1d96c 100644
--- a/eng/Packages.Data.props
+++ b/eng/Packages.Data.props
@@ -175,7 +175,7 @@
-->
-
+
diff --git a/sdk/core/Azure.Core/perf/Serializations/JsonBenchmark.cs b/sdk/core/Azure.Core/perf/Serializations/JsonBenchmark.cs
index 3bf0044b1ae2..d3f64eb3c2b0 100644
--- a/sdk/core/Azure.Core/perf/Serializations/JsonBenchmark.cs
+++ b/sdk/core/Azure.Core/perf/Serializations/JsonBenchmark.cs
@@ -123,11 +123,23 @@ public void Serialize_PublicInterface()
[Benchmark]
[BenchmarkCategory("Internal")]
- public T Deserialize_Internal()
+ public T Deserialize_Internal_JsonElement()
{
return Deserialize(_jsonDocument.RootElement);
}
+ protected virtual T Deserialize(BinaryData data)
+ {
+ return Deserialize(JsonDocument.Parse(data).RootElement);
+ }
+
+ [Benchmark]
+ [BenchmarkCategory("Internal")]
+ public T Deserialize_Internal_BinaryData()
+ {
+ return Deserialize(_data);
+ }
+
[Benchmark]
[BenchmarkCategory("Cast")]
public T Deserialize_ExplicitCast()
@@ -189,5 +201,21 @@ public void JsonDocumentFromBinaryData()
{
using var doc = JsonDocument.Parse(_data);
}
+
+ protected virtual void ModifyValues(T model) { }
+
+ [Benchmark]
+ [BenchmarkCategory("Usage")]
+ public void EndToEndUseCase()
+ {
+ // Instantiate an input model
+ T model = ModelSerializer.Deserialize(_data);
+
+ // Set properties on it
+ ModifyValues(model);
+
+ // Send it over the wire - serialize
+ RequestContent content = CastToRequestContent();
+ }
}
}
diff --git a/sdk/core/Azure.Core/perf/Serializations/SimplePatchModelBenchmark.cs b/sdk/core/Azure.Core/perf/Serializations/SimplePatchModelBenchmark.cs
new file mode 100644
index 000000000000..34e74d3ba48c
--- /dev/null
+++ b/sdk/core/Azure.Core/perf/Serializations/SimplePatchModelBenchmark.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.IO;
+using System.Text.Json;
+using Azure.Core.Tests.PatchModels;
+
+namespace Azure.Core.Perf.Serializations
+{
+ public class SimplePatchModelBenchmark : JsonBenchmark
+ {
+ protected override SimplePatchModel CastFromResponse() => (SimplePatchModel)_response;
+
+ protected override RequestContent CastToRequestContent() => _model;
+
+ protected override SimplePatchModel Deserialize(JsonElement jsonElement)
+ {
+ using Stream stream = new MemoryStream();
+ using Utf8JsonWriter writer = new(stream);
+ jsonElement.WriteTo(writer);
+ writer.Flush();
+ stream.Position = 0;
+ return Deserialize(BinaryData.FromStream(stream));
+ }
+
+ protected override SimplePatchModel Deserialize(BinaryData data) => SimplePatchModel.Deserialize(data);
+
+ protected override void Serialize(Utf8JsonWriter writer) => _model.Serialize(writer);
+
+ protected override string JsonFileName => "SimplePatchModel.json";
+
+ protected override void ModifyValues(SimplePatchModel model)
+ {
+ model.Name = "xyz";
+ model.Count = 2;
+ }
+ }
+}
diff --git a/sdk/core/Azure.Core/perf/Serializations/SimpleStandardModelBenchmark.cs b/sdk/core/Azure.Core/perf/Serializations/SimpleStandardModelBenchmark.cs
new file mode 100644
index 000000000000..794bda93ec28
--- /dev/null
+++ b/sdk/core/Azure.Core/perf/Serializations/SimpleStandardModelBenchmark.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System.Text.Json;
+using Azure.Core.Tests.PatchModels;
+
+namespace Azure.Core.Perf.Serializations
+{
+ public class SimpleStandardModelBenchmark : JsonBenchmark
+ {
+ protected override SimpleStandardModel CastFromResponse() => (SimpleStandardModel)_response;
+
+ protected override RequestContent CastToRequestContent() => _model;
+
+ protected override SimpleStandardModel Deserialize(JsonElement jsonElement)
+ {
+ return SimpleStandardModel.DeserializeSimpleStandardModel(jsonElement, new("J"));
+ }
+
+ protected override void Serialize(Utf8JsonWriter writer) => _model.Serialize(writer);
+
+ protected override string JsonFileName => "SimpleStandardModel.json";
+
+ protected override void ModifyValues(SimpleStandardModel model)
+ {
+ model.Name = "xyz";
+ model.Count = 2;
+ }
+ }
+}
diff --git a/sdk/core/Azure.Core/src/Azure.Core.csproj b/sdk/core/Azure.Core/src/Azure.Core.csproj
index 77c33dda5ecd..a4490481576c 100644
--- a/sdk/core/Azure.Core/src/Azure.Core.csproj
+++ b/sdk/core/Azure.Core/src/Azure.Core.csproj
@@ -64,6 +64,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/sdk/core/Azure.Core/src/DynamicData/MutableJsonChange.cs b/sdk/core/Azure.Core/src/Shared/MutableJsonChange.cs
similarity index 99%
rename from sdk/core/Azure.Core/src/DynamicData/MutableJsonChange.cs
rename to sdk/core/Azure.Core/src/Shared/MutableJsonChange.cs
index 4fc4cebafa20..2d781ad4f4f0 100644
--- a/sdk/core/Azure.Core/src/DynamicData/MutableJsonChange.cs
+++ b/sdk/core/Azure.Core/src/Shared/MutableJsonChange.cs
@@ -5,6 +5,8 @@
using System.Buffers;
using System.Text.Json;
+#nullable enable
+
namespace Azure.Core.Json
{
internal struct MutableJsonChange
diff --git a/sdk/core/Azure.Core/src/DynamicData/MutableJsonChangeKind.cs b/sdk/core/Azure.Core/src/Shared/MutableJsonChangeKind.cs
similarity index 93%
rename from sdk/core/Azure.Core/src/DynamicData/MutableJsonChangeKind.cs
rename to sdk/core/Azure.Core/src/Shared/MutableJsonChangeKind.cs
index 645b31151a06..5697371054de 100644
--- a/sdk/core/Azure.Core/src/DynamicData/MutableJsonChangeKind.cs
+++ b/sdk/core/Azure.Core/src/Shared/MutableJsonChangeKind.cs
@@ -1,6 +1,8 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
+#nullable enable
+
namespace Azure.Core.Json
{
internal enum MutableJsonChangeKind
diff --git a/sdk/core/Azure.Core/src/DynamicData/MutableJsonDocument.ChangeTracker.cs b/sdk/core/Azure.Core/src/Shared/MutableJsonDocument.ChangeTracker.cs
similarity index 99%
rename from sdk/core/Azure.Core/src/DynamicData/MutableJsonDocument.ChangeTracker.cs
rename to sdk/core/Azure.Core/src/Shared/MutableJsonDocument.ChangeTracker.cs
index 7cf52ca9708f..6e08419ac9ac 100644
--- a/sdk/core/Azure.Core/src/DynamicData/MutableJsonDocument.ChangeTracker.cs
+++ b/sdk/core/Azure.Core/src/Shared/MutableJsonDocument.ChangeTracker.cs
@@ -6,6 +6,8 @@
using System.Diagnostics;
using System.Text.Json;
+#nullable enable
+
namespace Azure.Core.Json
{
internal partial class MutableJsonDocument
diff --git a/sdk/core/Azure.Core/src/DynamicData/MutableJsonDocument.cs b/sdk/core/Azure.Core/src/Shared/MutableJsonDocument.cs
similarity index 96%
rename from sdk/core/Azure.Core/src/DynamicData/MutableJsonDocument.cs
rename to sdk/core/Azure.Core/src/Shared/MutableJsonDocument.cs
index 8fc4828ae2e3..28fa7c6c23ee 100644
--- a/sdk/core/Azure.Core/src/DynamicData/MutableJsonDocument.cs
+++ b/sdk/core/Azure.Core/src/Shared/MutableJsonDocument.cs
@@ -8,6 +8,8 @@
using System.Text.Json;
using System.Text.Json.Serialization;
+#nullable enable
+
namespace Azure.Core.Json
{
///
@@ -16,6 +18,9 @@ namespace Azure.Core.Json
[JsonConverter(typeof(MutableJsonDocumentConverter))]
internal sealed partial class MutableJsonDocument : IDisposable
{
+ private static readonly ReadOnlyMemory _emptyJson = "{}"u8.ToArray();
+ public static ReadOnlyMemory EmptyJson => _emptyJson;
+
private readonly ReadOnlyMemory _original;
private readonly JsonDocument _originalDocument;
@@ -199,11 +204,12 @@ public void Dispose()
_originalDocument.Dispose();
}
+ private static JsonSerializerOptions DefaultSerializerOptions = new JsonSerializerOptions();
private MutableJsonDocument(JsonDocument document, ReadOnlyMemory utf8Json, JsonSerializerOptions? serializerOptions)
{
_originalDocument = document;
_original = utf8Json;
- _serializerOptions = serializerOptions ?? new JsonSerializerOptions();
+ _serializerOptions = serializerOptions ?? DefaultSerializerOptions;
}
private class MutableJsonDocumentConverter : JsonConverter
diff --git a/sdk/core/Azure.Core/src/DynamicData/MutableJsonElement.ArrayEnumerator.cs b/sdk/core/Azure.Core/src/Shared/MutableJsonElement.ArrayEnumerator.cs
similarity index 99%
rename from sdk/core/Azure.Core/src/DynamicData/MutableJsonElement.ArrayEnumerator.cs
rename to sdk/core/Azure.Core/src/Shared/MutableJsonElement.ArrayEnumerator.cs
index 9df30484541e..aecd0a1e516a 100644
--- a/sdk/core/Azure.Core/src/DynamicData/MutableJsonElement.ArrayEnumerator.cs
+++ b/sdk/core/Azure.Core/src/Shared/MutableJsonElement.ArrayEnumerator.cs
@@ -6,6 +6,8 @@
using System.Diagnostics;
using System.Text.Json;
+#nullable enable
+
namespace Azure.Core.Json
{
internal partial struct MutableJsonElement
diff --git a/sdk/core/Azure.Core/src/DynamicData/MutableJsonElement.ObjectEnumerator.cs b/sdk/core/Azure.Core/src/Shared/MutableJsonElement.ObjectEnumerator.cs
similarity index 99%
rename from sdk/core/Azure.Core/src/DynamicData/MutableJsonElement.ObjectEnumerator.cs
rename to sdk/core/Azure.Core/src/Shared/MutableJsonElement.ObjectEnumerator.cs
index 4acb99fedc98..201f8440ad84 100644
--- a/sdk/core/Azure.Core/src/DynamicData/MutableJsonElement.ObjectEnumerator.cs
+++ b/sdk/core/Azure.Core/src/Shared/MutableJsonElement.ObjectEnumerator.cs
@@ -7,6 +7,8 @@
using System.Diagnostics;
using System.Text.Json;
+#nullable enable
+
namespace Azure.Core.Json
{
///
diff --git a/sdk/core/Azure.Core/src/DynamicData/MutableJsonElement.WriteTo.MergePatch.cs b/sdk/core/Azure.Core/src/Shared/MutableJsonElement.WriteTo.MergePatch.cs
similarity index 99%
rename from sdk/core/Azure.Core/src/DynamicData/MutableJsonElement.WriteTo.MergePatch.cs
rename to sdk/core/Azure.Core/src/Shared/MutableJsonElement.WriteTo.MergePatch.cs
index bac8a3b114c6..6a46d3d77ef7 100644
--- a/sdk/core/Azure.Core/src/DynamicData/MutableJsonElement.WriteTo.MergePatch.cs
+++ b/sdk/core/Azure.Core/src/Shared/MutableJsonElement.WriteTo.MergePatch.cs
@@ -5,6 +5,8 @@
using System.Diagnostics;
using System.Text.Json;
+#nullable enable
+
namespace Azure.Core.Json
{
internal partial struct MutableJsonElement
diff --git a/sdk/core/Azure.Core/src/DynamicData/MutableJsonElement.WriteTo.cs b/sdk/core/Azure.Core/src/Shared/MutableJsonElement.WriteTo.cs
similarity index 99%
rename from sdk/core/Azure.Core/src/DynamicData/MutableJsonElement.WriteTo.cs
rename to sdk/core/Azure.Core/src/Shared/MutableJsonElement.WriteTo.cs
index cb5bbd43363f..75ae5a7d61d9 100644
--- a/sdk/core/Azure.Core/src/DynamicData/MutableJsonElement.WriteTo.cs
+++ b/sdk/core/Azure.Core/src/Shared/MutableJsonElement.WriteTo.cs
@@ -5,6 +5,8 @@
using System.Collections.Generic;
using System.Text.Json;
+#nullable enable
+
namespace Azure.Core.Json
{
internal partial struct MutableJsonElement
diff --git a/sdk/core/Azure.Core/src/DynamicData/MutableJsonElement.cs b/sdk/core/Azure.Core/src/Shared/MutableJsonElement.cs
similarity index 99%
rename from sdk/core/Azure.Core/src/DynamicData/MutableJsonElement.cs
rename to sdk/core/Azure.Core/src/Shared/MutableJsonElement.cs
index f02f682faf3f..c0ec625e63ed 100644
--- a/sdk/core/Azure.Core/src/DynamicData/MutableJsonElement.cs
+++ b/sdk/core/Azure.Core/src/Shared/MutableJsonElement.cs
@@ -9,6 +9,8 @@
using System.Text.Json;
using System.Text.Json.Serialization;
+#nullable enable
+
namespace Azure.Core.Json
{
///
diff --git a/sdk/core/Azure.Core/tests/common/Azure.Core.Tests.Common.csproj b/sdk/core/Azure.Core/tests/common/Azure.Core.Tests.Common.csproj
index cb8e22f45568..fbd22a68ac2d 100644
--- a/sdk/core/Azure.Core/tests/common/Azure.Core.Tests.Common.csproj
+++ b/sdk/core/Azure.Core/tests/common/Azure.Core.Tests.Common.csproj
@@ -18,6 +18,12 @@
+
+ Always
+
+
+ Always
+
Always
diff --git a/sdk/core/Azure.Core/tests/common/TestData/SimplePatchModel.json b/sdk/core/Azure.Core/tests/common/TestData/SimplePatchModel.json
new file mode 100644
index 000000000000..3bb777e63da3
--- /dev/null
+++ b/sdk/core/Azure.Core/tests/common/TestData/SimplePatchModel.json
@@ -0,0 +1,5 @@
+{
+ "name": "abc",
+ "count": 1,
+ "updatedOn": "2023-10-19T10:19:10.0190001Z"
+}
diff --git a/sdk/core/Azure.Core/tests/common/TestData/SimpleStandardModel.json b/sdk/core/Azure.Core/tests/common/TestData/SimpleStandardModel.json
new file mode 100644
index 000000000000..3bb777e63da3
--- /dev/null
+++ b/sdk/core/Azure.Core/tests/common/TestData/SimpleStandardModel.json
@@ -0,0 +1,5 @@
+{
+ "name": "abc",
+ "count": 1,
+ "updatedOn": "2023-10-19T10:19:10.0190001Z"
+}
diff --git a/sdk/core/Azure.Core/tests/public/Azure.Core.Tests.Public.csproj b/sdk/core/Azure.Core/tests/public/Azure.Core.Tests.Public.csproj
index dd5cf6cebc4f..d08a1c3d17ce 100644
--- a/sdk/core/Azure.Core/tests/public/Azure.Core.Tests.Public.csproj
+++ b/sdk/core/Azure.Core/tests/public/Azure.Core.Tests.Public.csproj
@@ -22,8 +22,16 @@
-
+
+
+
+
+
+
+
+
+
diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/ChildPatchModel.cs b/sdk/core/Azure.Core/tests/public/PatchModels/ChildPatchModel.cs
new file mode 100644
index 000000000000..90ce3ff6411f
--- /dev/null
+++ b/sdk/core/Azure.Core/tests/public/PatchModels/ChildPatchModel.cs
@@ -0,0 +1,64 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Azure.Core.Json;
+
+namespace Azure.Core.Tests.PatchModels
+{
+ ///
+ /// This model illustrates a nested child model in a parent model.
+ ///
+ public partial class ChildPatchModel
+ {
+#pragma warning disable AZC0020 // Avoid using banned types in libraries
+ private readonly MutableJsonElement _element;
+
+ // Note: A child patch model doesn't have a public constructor.
+ //
+ // When a nested model is also an input to a service method, it will
+ // need to have a public constructor. When this happens, the parent
+ // model it is also part of will not allow it to be set, to ensure the
+ // MutableJsonElements point to the same root MutableJsonDocument when
+ // they are part of the same input, so the Patch JSON will be correct.
+
+ /// Serialization constructor.
+ ///
+ internal ChildPatchModel(MutableJsonElement element)
+ {
+ _element = element;
+ }
+
+ ///
+ /// Optional string property corresponding to JSON """{"a": "aaa"}""".
+ ///
+ public string A
+ {
+ get
+ {
+ if (_element.TryGetProperty("a", out MutableJsonElement value))
+ {
+ return value.GetString();
+ }
+ return null;
+ }
+ set => _element.SetProperty("a", value);
+ }
+
+ ///
+ /// Optional string property corresponding to JSON """{"b": "bbb"}""".
+ ///
+ public string B
+ {
+ get
+ {
+ if (_element.TryGetProperty("b", out MutableJsonElement value))
+ {
+ return value.GetString();
+ }
+ return null;
+ }
+ set => _element.SetProperty("b", value);
+ }
+#pragma warning restore AZC0020 // Avoid using banned types in libraries
+ }
+}
diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/ParentPatchModel.Serialization.cs b/sdk/core/Azure.Core/tests/public/PatchModels/ParentPatchModel.Serialization.cs
new file mode 100644
index 000000000000..527dab7c5c01
--- /dev/null
+++ b/sdk/core/Azure.Core/tests/public/PatchModels/ParentPatchModel.Serialization.cs
@@ -0,0 +1,72 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Text.Json;
+using Azure.Core.Json;
+using Azure.Core.Serialization;
+
+namespace Azure.Core.Tests.PatchModels
+{
+ public partial class ParentPatchModel : IModelJsonSerializable
+ {
+ ParentPatchModel IModelJsonSerializable.Deserialize(ref Utf8JsonReader reader, ModelSerializerOptions options)
+ {
+ PatchModelHelper.ValidateFormat(this, options.Format);
+
+ return Deserialize(ref reader, options);
+ }
+
+ private static ParentPatchModel Deserialize(ref Utf8JsonReader reader, ModelSerializerOptions options)
+ {
+ MutableJsonDocument mdoc = MutableJsonDocument.Parse(ref reader);
+ return new ParentPatchModel(mdoc.RootElement);
+ }
+
+ ParentPatchModel IModelSerializable.Deserialize(BinaryData data, ModelSerializerOptions options)
+ {
+ PatchModelHelper.ValidateFormat(this, options.Format);
+
+ return Deserialize(data, options);
+ }
+
+ private static ParentPatchModel Deserialize(BinaryData data, ModelSerializerOptions options)
+ {
+ MutableJsonDocument mdoc = MutableJsonDocument.Parse(data);
+ return new ParentPatchModel(mdoc.RootElement);
+ }
+
+ void IModelJsonSerializable.Serialize(Utf8JsonWriter writer, ModelSerializerOptions options)
+ {
+ PatchModelHelper.ValidateFormat(this, options.Format);
+
+ switch (options.Format.ToString())
+ {
+ case "J":
+ case "W":
+ _element.WriteTo(writer, "J");
+ break;
+ case "P":
+ _element.WriteTo(writer, "P");
+ break;
+ default:
+ // Exception was thrown by ValidateFormat.
+ break;
+ }
+ }
+
+ BinaryData IModelSerializable.Serialize(ModelSerializerOptions options)
+ {
+ PatchModelHelper.ValidateFormat(this, options.Format);
+
+ return ModelSerializer.SerializeCore(this, options);
+ }
+
+ public static explicit operator ParentPatchModel(Response response)
+ {
+ Argument.AssertNotNull(response, nameof(response));
+
+ return Deserialize(response.Content, ModelSerializerOptions.DefaultWireOptions);
+ }
+ }
+}
diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/ParentPatchModel.cs b/sdk/core/Azure.Core/tests/public/PatchModels/ParentPatchModel.cs
new file mode 100644
index 000000000000..edeafab91c3c
--- /dev/null
+++ b/sdk/core/Azure.Core/tests/public/PatchModels/ParentPatchModel.cs
@@ -0,0 +1,81 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using Azure.Core.Json;
+
+namespace Azure.Core.Tests.PatchModels
+{
+ ///
+ /// This model illustrates a patch model with properties that are nested models.
+ ///
+ public partial class ParentPatchModel
+ {
+#pragma warning disable AZC0020 // Avoid using banned types in libraries
+ private readonly MutableJsonElement _element;
+
+ ///
+ /// Public constructor.
+ ///
+ public ParentPatchModel()
+ {
+ _element = MutableJsonDocument.Parse(MutableJsonDocument.EmptyJson).RootElement;
+ }
+
+ ///
+ /// Serialization constructor.
+ ///
+ ///
+ internal ParentPatchModel(MutableJsonElement element)
+ {
+ _element = element;
+ }
+
+ ///
+ /// Optional string property corresponding to JSON """{"id": "abc"}""".
+ ///
+ public string Id
+ {
+ get
+ {
+ if (_element.TryGetProperty("id", out MutableJsonElement value))
+ {
+ return value.GetString();
+ }
+ return null;
+ }
+ set => _element.SetProperty("id", value);
+ }
+
+ private ChildPatchModel _child;
+ ///
+ /// Optional ChildPatchModel property corresponding to JSON """{"child": {"a":"aa", "b": "bb"}}""".
+ ///
+ public ChildPatchModel Child
+ {
+ get
+ {
+ if (_child == null)
+ {
+ if (!_element.TryGetProperty("child", out MutableJsonElement element))
+ {
+ _element.SetProperty("child", new { });
+ element = _element.GetProperty("child");
+ }
+
+ _child = new ChildPatchModel(element);
+ }
+
+ return _child;
+ }
+
+ // Note: a child patch model isn't settable on the parent.
+ // This is because its _element property needs to have the
+ // same root MutableJsonDocument as the parent.
+ //
+ // It's unclear how we would plug it in after the fact if we wanted
+ // to make an instance of the child model something that could be
+ // used in multiple places.
+ }
+#pragma warning restore AZC0020 // Avoid using banned types in libraries
+ }
+}
diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/ParentPatchModelTests.cs b/sdk/core/Azure.Core/tests/public/PatchModels/ParentPatchModelTests.cs
new file mode 100644
index 000000000000..a0469e6e640e
--- /dev/null
+++ b/sdk/core/Azure.Core/tests/public/PatchModels/ParentPatchModelTests.cs
@@ -0,0 +1,47 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using Azure.Core.Serialization;
+using Azure.Core.Tests.PatchModels;
+using NUnit.Framework;
+
+namespace Azure.Core.Tests.Public.ModelSerializationTests
+{
+ internal class ParentPatchModelTests : ModelJsonTests
+ {
+ protected override string JsonPayload => """
+ {
+ "id": "abc",
+ "child": {"a": "aa", "b": "bb"}
+ }
+ """;
+
+ protected override string WirePayload => JsonPayload;
+
+ protected override Func ToRequestContent => m => m == null ? null: RequestContent.Create(m, ModelSerializerOptions.DefaultWireOptions);
+
+ protected override Func FromResponse => r => (ParentPatchModel)r;
+
+ protected override void CompareModels(ParentPatchModel model, ParentPatchModel model2, ModelSerializerFormat format)
+ {
+ Assert.AreEqual(model.Child.A, model2.Child.A);
+ Assert.AreEqual(model.Child.B, model2.Child.B);
+ Assert.AreEqual(model.Id, model2.Id);
+ }
+
+ protected override string GetExpectedResult(ModelSerializerFormat format)
+ {
+ return RemoveWhitespace(JsonPayload);
+ }
+
+ protected override void VerifyModel(ParentPatchModel model, ModelSerializerFormat format)
+ {
+ Assert.AreEqual("aa", model.Child.A);
+ Assert.AreEqual("bb", model.Child.B);
+ Assert.AreEqual("abc", model.Id);
+ }
+
+ private static string RemoveWhitespace(string value) => value.Replace("\r", "").Replace("\n", "").Replace(" ", "");
+ }
+}
diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/PatchModelHelper.cs b/sdk/core/Azure.Core/tests/public/PatchModels/PatchModelHelper.cs
new file mode 100644
index 000000000000..0ba26130d13f
--- /dev/null
+++ b/sdk/core/Azure.Core/tests/public/PatchModels/PatchModelHelper.cs
@@ -0,0 +1,23 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Text.Json;
+using Azure.Core.Json;
+using Azure.Core.Serialization;
+
+namespace Azure.Core.Tests.PatchModels
+{
+ internal class PatchModelHelper
+ {
+ // TODO: Move this into the public interface as a separate PR
+ public static void ValidateFormat(IModelSerializable model, ModelSerializerFormat format)
+ {
+ bool isValidPatchFormat = model is IModelJsonSerializable && format == "P";
+ if (!isValidPatchFormat)
+ {
+ ModelSerializerHelper.ValidateFormat(model, format);
+ }
+ }
+ }
+}
diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/PatchModelTests.cs b/sdk/core/Azure.Core/tests/public/PatchModels/PatchModelTests.cs
new file mode 100644
index 000000000000..bfc6eaf7c4bb
--- /dev/null
+++ b/sdk/core/Azure.Core/tests/public/PatchModels/PatchModelTests.cs
@@ -0,0 +1,294 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.IO;
+using System.Text.Json;
+using Azure.Core.Serialization;
+using Azure.Core.Tests.PatchModels;
+using NUnit.Framework;
+
+namespace Azure.Core.Tests.Public
+{
+ public class PatchModelTests
+ {
+ [Test]
+ public void CanPatchIntProperty()
+ {
+ SimplePatchModel model = new();
+ model.Count = 2;
+
+ ValidatePatch("""{"count":2}""", model);
+ }
+
+ [Test]
+ public void CanPatchStringProperty()
+ {
+ SimplePatchModel model = new();
+ model.Name = "abc";
+
+ ValidatePatch("""{"name":"abc"}""", model);
+ }
+
+ [Test]
+ public void CanPatchDateTimeProperty()
+ {
+ DateTimeOffset updateTime = DateTimeOffset.Parse("2023-10-19T10:19:10.0190001Z");
+
+ SimplePatchModel model = new();
+ model.UpdatedOn = updateTime;
+
+ ValidatePatch($"{{\"updatedOn\":\"{updateTime:O}\"}}", model);
+ }
+
+ [Test]
+ public void CanRoundTripSimpleModel()
+ {
+ BinaryData json = BinaryData.FromString("""
+ {
+ "name": "abc",
+ "count": 1
+ }
+ """);
+
+ SimplePatchModel model = ModelSerializer.Deserialize(json);
+
+ Assert.AreEqual(1, model.Count);
+ Assert.AreEqual("abc", model.Name);
+
+ model.Name = "xyz";
+ model.Count = 2;
+
+ ValidatePatch("""{"count":2, "name":"xyz"}""", model);
+ }
+
+ #region Standard model
+
+ [Test]
+ public void CanSetIntProperty()
+ {
+ JsonDocument doc = JsonDocument.Parse("""
+ {
+ "name": "abc",
+ "count": 1,
+ "updatedOn": "2023-10-19T10:19:10.0190001Z"
+ }
+ """);
+ SimpleStandardModel model = SimpleStandardModel.DeserializeSimpleStandardModel(doc.RootElement, new ModelSerializerOptions("J"));
+ model.Count = 2;
+
+ ValidateSerialize("""
+ {
+ "name": "abc",
+ "count": 2,
+ "updatedOn": "2023-10-19T10:19:10.0190001Z"
+ }
+ """, model);
+ }
+
+ [Test]
+ public void CanSetStringProperty()
+ {
+ JsonDocument doc = JsonDocument.Parse("""
+ {
+ "name": "abc",
+ "count": 1,
+ "updatedOn": "2023-10-19T10:19:10.0190001Z"
+ }
+ """);
+ SimpleStandardModel model = SimpleStandardModel.DeserializeSimpleStandardModel(doc.RootElement, new ModelSerializerOptions("J"));
+ model.Name = "xyz";
+
+ ValidateSerialize("""
+ {
+ "name": "xyz",
+ "count": 1,
+ "updatedOn": "2023-10-19T10:19:10.0190001Z"
+ }
+ """, model);
+ }
+
+ [Test]
+ public void CanSetDateTimeProperty()
+ {
+ DateTimeOffset updateTime = DateTimeOffset.Parse("2023-10-20T10:20:10.0190001Z");
+ JsonDocument doc = JsonDocument.Parse("""
+ {
+ "name": "abc",
+ "count": 1,
+ "updatedOn": "2023-10-19T10:19:10.0190001Z"
+ }
+ """);
+
+ SimpleStandardModel model = SimpleStandardModel.DeserializeSimpleStandardModel(doc.RootElement, new ModelSerializerOptions("J"));
+ model.UpdatedOn = updateTime;
+
+ ValidateSerialize("""
+ {
+ "name": "abc",
+ "count": 1,
+ "updatedOn": "2023-10-20T10:20:10.0190001Z"
+ }
+ """, model);
+ }
+
+ [Test]
+ public void CanRoundTripSimpleStandardModel()
+ {
+ BinaryData json = BinaryData.FromString("""
+ {
+ "name": "abc",
+ "count": 1
+ }
+ """);
+
+ SimplePatchModel model = ModelSerializer.Deserialize(json);
+
+ Assert.AreEqual(1, model.Count);
+ Assert.AreEqual("abc", model.Name);
+
+ model.Name = "xyz";
+ model.Count = 2;
+
+ ValidatePatch("""{"count":2, "name":"xyz"}""", model);
+ }
+ #endregion standard model
+
+ [Test]
+ public void CanPatchNestedModel()
+ {
+ ParentPatchModel model = new();
+
+ model.Child.B = "bb";
+
+ ValidatePatch("""{"child": {"b": "bb"}}""", model);
+
+ model.Child.A = "aa";
+
+ ValidatePatch("""{"child": {"a": "aa", "b": "bb"}}""", model);
+ }
+
+ [Test]
+ public void CanPatchNestedModelOneProperty()
+ {
+ ParentPatchModel model = new();
+
+ model.Child.A = "aa";
+
+ ValidatePatch("""{"child": {"a": "aa"}}""", model);
+ }
+
+ [Test]
+ public void CanPatchNestedModelOnePropertyAndChangeIt()
+ {
+ ParentPatchModel model = new();
+
+ model.Child.A = "a1";
+
+ ValidatePatch("""{"child": {"a": "a1"}}""", model);
+
+ model.Child.A = "a2";
+
+ ValidatePatch("""{"child": {"a": "a2"}}""", model);
+ }
+
+ [Test]
+ public void CanPatchNestedModelInterleaveChanges()
+ {
+ ParentPatchModel model = new();
+
+ model.Id = "id1";
+ model.Child.B = "b1";
+ model.Child.A = "a1";
+
+ ValidatePatch("""{"child": {"a": "a1", "b": "b1"}, "id": "id1"}""", model);
+
+ model.Child.A = "a2";
+ model.Id = "id2";
+
+ ValidatePatch("""{"child": {"a": "a2", "b": "b1"}, "id": "id2"}""", model);
+
+ model.Child.A = null;
+ model.Child.B = null;
+
+ ValidatePatch("""{"child": {"a": null, "b": null}, "id": "id2"}""", model);
+ }
+
+ [Test]
+ public void CanRoundTripNestedModel()
+ {
+ BinaryData json = BinaryData.FromString("""
+ {
+ "id": "123",
+ "child": {
+ "a": "aa",
+ "b": "bb"
+ }
+ }
+ """);
+
+ ParentPatchModel model = ModelSerializer.Deserialize(json);
+
+ Assert.AreEqual("123", model.Id);
+ Assert.AreEqual("aa", model.Child.A);
+ Assert.AreEqual("bb", model.Child.B);
+
+ ValidateSerialize("""{"id": "123", "child": {"a": "aa", "b": "bb"}}""", model);
+ ValidatePatch(string.Empty, model);
+
+ model.Child.A = "a2";
+ model.Child.B = null;
+
+ ValidateSerialize("""{"id": "123", "child": {"a": "a2", "b": null}}""", model);
+ ValidatePatch("""{"child": {"a": "a2", "b": null}}""", model);
+ }
+
+ #region Helpers
+ private static void ValidateSerialize(string expected, IModelJsonSerializable model)
+ {
+ using Stream stream = new MemoryStream();
+ using Utf8JsonWriter writer = new(stream);
+ model.Serialize(writer, new ModelSerializerOptions("J"));
+ writer.Flush();
+ stream.Position = 0;
+
+ string actual = BinaryData.FromStream(stream).ToString();
+
+ AreEqualJson(expected, actual);
+ }
+
+ private static void ValidatePatch(string expected, IModelJsonSerializable model)
+ {
+ using Stream stream = new MemoryStream();
+ using Utf8JsonWriter writer = new(stream);
+ model.Serialize(writer, new ModelSerializerOptions("P"));
+ writer.Flush();
+ stream.Position = 0;
+
+ string actual = BinaryData.FromStream(stream).ToString();
+
+ if (expected.Length == 0)
+ {
+ Assert.AreEqual(expected, actual);
+ return;
+ }
+
+ AreEqualJson(expected, actual);
+ }
+
+ private static void AreEqualJson(string expected, string actual)
+ {
+ JsonDocument doc = JsonDocument.Parse(expected);
+
+ using MemoryStream stream = new();
+ using Utf8JsonWriter writer = new(stream);
+ doc.WriteTo(writer);
+ writer.Flush();
+ stream.Position = 0;
+ BinaryData buffer = BinaryData.FromStream(stream);
+
+ Assert.AreEqual(buffer.ToString(), actual);
+ }
+ #endregion
+ }
+}
diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/SimplePatchModel.Serialization.cs b/sdk/core/Azure.Core/tests/public/PatchModels/SimplePatchModel.Serialization.cs
new file mode 100644
index 000000000000..ffa78f9c896c
--- /dev/null
+++ b/sdk/core/Azure.Core/tests/public/PatchModels/SimplePatchModel.Serialization.cs
@@ -0,0 +1,81 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Text.Json;
+using Azure.Core.Json;
+using Azure.Core.Serialization;
+
+namespace Azure.Core.Tests.PatchModels
+{
+ public partial class SimplePatchModel : IModelJsonSerializable, IUtf8JsonSerializable
+ {
+ SimplePatchModel IModelJsonSerializable.Deserialize(ref Utf8JsonReader reader, ModelSerializerOptions options)
+ {
+ PatchModelHelper.ValidateFormat(this, options.Format);
+
+ return Deserialize(ref reader, options);
+ }
+
+ private static SimplePatchModel Deserialize(ref Utf8JsonReader reader, ModelSerializerOptions options)
+ {
+ MutableJsonDocument mdoc = MutableJsonDocument.Parse(ref reader);
+ return new SimplePatchModel(mdoc.RootElement);
+ }
+
+ SimplePatchModel IModelSerializable.Deserialize(BinaryData data, ModelSerializerOptions options)
+ {
+ PatchModelHelper.ValidateFormat(this, options.Format);
+
+ return Deserialize(data, options);
+ }
+
+ private static SimplePatchModel Deserialize(BinaryData data, ModelSerializerOptions options)
+ {
+ MutableJsonDocument mdoc = MutableJsonDocument.Parse(data);
+ return new SimplePatchModel(mdoc.RootElement);
+ }
+
+ void IModelJsonSerializable.Serialize(Utf8JsonWriter writer, ModelSerializerOptions options)
+ {
+ PatchModelHelper.ValidateFormat(this, options.Format);
+
+ switch (options.Format.ToString())
+ {
+ case "J":
+ case "W":
+ _element.WriteTo(writer, "J");
+ break;
+ case "P":
+ _element.WriteTo(writer, "P");
+ break;
+ default:
+ // Exception was thrown by ValidateFormat.
+ break;
+ }
+ }
+
+ BinaryData IModelSerializable.Serialize(ModelSerializerOptions options)
+ {
+ PatchModelHelper.ValidateFormat(this, options.Format);
+
+ return ModelSerializer.SerializeCore(this, options);
+ }
+
+ public static implicit operator RequestContent(SimplePatchModel model)
+ => RequestContent.Create(model, ModelSerializerOptions.DefaultWireOptions);
+
+ public static explicit operator SimplePatchModel(Response response)
+ {
+ Argument.AssertNotNull(response, nameof(response));
+
+ return Deserialize(response.Content, ModelSerializerOptions.DefaultWireOptions);
+ }
+
+ void IUtf8JsonSerializable.Write(Utf8JsonWriter writer) => ((IModelJsonSerializable)this).Serialize(writer, ModelSerializerOptions.DefaultWireOptions);
+
+ // TODO: Temp for pef tests
+ public void Serialize(Utf8JsonWriter writer) => ((IUtf8JsonSerializable)this).Write(writer);
+ public static SimplePatchModel Deserialize(BinaryData data) => Deserialize(data, ModelSerializerOptions.DefaultWireOptions);
+ }
+}
diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/SimplePatchModel.cs b/sdk/core/Azure.Core/tests/public/PatchModels/SimplePatchModel.cs
new file mode 100644
index 000000000000..74a329c5a2a1
--- /dev/null
+++ b/sdk/core/Azure.Core/tests/public/PatchModels/SimplePatchModel.cs
@@ -0,0 +1,83 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using Azure.Core.Json;
+
+namespace Azure.Core.Tests.PatchModels
+{
+ ///
+ /// This model illustrates optional read/write "primitive" properties.
+ ///
+ public partial class SimplePatchModel
+ {
+#pragma warning disable AZC0020 // Avoid using banned types in libraries
+ private readonly MutableJsonElement _element;
+
+ ///
+ /// Public constructor.
+ ///
+ public SimplePatchModel()
+ {
+ _element = MutableJsonDocument.Parse(MutableJsonDocument.EmptyJson).RootElement;
+ }
+
+ ///
+ /// Serialization constructor.
+ ///
+ ///
+ internal SimplePatchModel(MutableJsonElement element)
+ {
+ _element = element;
+ }
+
+ ///
+ /// Optional string property corresponding to JSON """{"name": "abc"}""".
+ ///
+ public string Name
+ {
+ get
+ {
+ if (_element.TryGetProperty("name", out MutableJsonElement value))
+ {
+ return value.GetString();
+ }
+ return null;
+ }
+ set => _element.SetProperty("name", value);
+ }
+
+ ///
+ /// Optional int property corresponding to JSON """{"count": 1}""".
+ ///
+ public int? Count
+ {
+ get
+ {
+ if (_element.TryGetProperty("count", out MutableJsonElement value))
+ {
+ return value.GetInt32();
+ }
+ return null;
+ }
+ set => _element.SetProperty("count", value);
+ }
+
+ ///
+ /// Optional DateTimeOffset property corresponding to JSON """{"updatedOn": "2020-06-25T17:44:37.6830000Z"}""".
+ ///
+ public DateTimeOffset? UpdatedOn
+ {
+ get
+ {
+ if (_element.TryGetProperty("updatedOn", out MutableJsonElement value))
+ {
+ return value.GetDateTimeOffset();
+ }
+ return null;
+ }
+ set => _element.SetProperty("updatedOn", value);
+ }
+#pragma warning restore AZC0020 // Avoid using banned types in libraries
+ }
+}
diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/SimplePatchModelTests.cs b/sdk/core/Azure.Core/tests/public/PatchModels/SimplePatchModelTests.cs
new file mode 100644
index 000000000000..1e846b7b2925
--- /dev/null
+++ b/sdk/core/Azure.Core/tests/public/PatchModels/SimplePatchModelTests.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using Azure.Core.Serialization;
+using Azure.Core.Tests.PatchModels;
+using NUnit.Framework;
+
+namespace Azure.Core.Tests.Public.ModelSerializationTests
+{
+ internal class SimplePatchModelTests : ModelJsonTests
+ {
+ protected override string JsonPayload => """
+ {
+ "name": "abc",
+ "count": 1,
+ "updatedOn": "2023-10-19T10:19:10.0190001Z"
+ }
+ """;
+
+ protected override string WirePayload => JsonPayload;
+
+ protected override Func ToRequestContent => m => m == null ? null: RequestContent.Create(m, ModelSerializerOptions.DefaultWireOptions);
+
+ protected override Func FromResponse => r => (SimplePatchModel)r;
+
+ protected override void CompareModels(SimplePatchModel model, SimplePatchModel model2, ModelSerializerFormat format)
+ {
+ Assert.AreEqual(model.Name, model2.Name);
+ Assert.AreEqual(model.Count, model2.Count);
+ Assert.AreEqual(model.UpdatedOn, model2.UpdatedOn);
+ }
+
+ protected override string GetExpectedResult(ModelSerializerFormat format)
+ {
+ return RemoveWhitespace(JsonPayload);
+ }
+
+ protected override void VerifyModel(SimplePatchModel model, ModelSerializerFormat format)
+ {
+ Assert.AreEqual("abc", model.Name);
+ Assert.AreEqual(1, model.Count);
+ Assert.AreEqual(DateTimeOffset.Parse("2023-10-19T10:19:10.0190001Z"), model.UpdatedOn);
+ }
+
+ private static string RemoveWhitespace(string value) => value.Replace("\r", "").Replace("\n", "").Replace(" ", "");
+ }
+}
diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/SimpleStandardModel.Serialization.cs b/sdk/core/Azure.Core/tests/public/PatchModels/SimpleStandardModel.Serialization.cs
new file mode 100644
index 000000000000..7d4fe4396b63
--- /dev/null
+++ b/sdk/core/Azure.Core/tests/public/PatchModels/SimpleStandardModel.Serialization.cs
@@ -0,0 +1,112 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Text.Json;
+using Azure.Core.Serialization;
+
+namespace Azure.Core.Tests.PatchModels
+{
+ public partial class SimpleStandardModel : IModelJsonSerializable, IUtf8JsonSerializable
+ {
+ public static SimpleStandardModel DeserializeSimpleStandardModel(JsonElement element, ModelSerializerOptions options)
+ {
+ options ??= ModelSerializerOptions.DefaultWireOptions;
+
+ if (element.ValueKind == JsonValueKind.Null)
+ {
+ return null;
+ }
+ Optional name = default;
+ Optional count = default;
+ Optional updatedOn = default;
+
+ foreach (var property in element.EnumerateObject())
+ {
+ if (property.NameEquals("name"u8))
+ {
+ name = property.Value.GetString();
+ continue;
+ }
+ if (property.NameEquals("count"u8))
+ {
+ count = property.Value.GetInt32();
+ continue;
+ }
+ if (property.NameEquals("updatedOn"u8))
+ {
+ updatedOn = property.Value.GetDateTimeOffset();
+ continue;
+ }
+ }
+ return new SimpleStandardModel(name, count, updatedOn);
+ }
+
+ SimpleStandardModel IModelJsonSerializable.Deserialize(ref Utf8JsonReader reader, ModelSerializerOptions options)
+ {
+ PatchModelHelper.ValidateFormat(this, options.Format);
+
+ using var doc = JsonDocument.ParseValue(ref reader);
+ return DeserializeSimpleStandardModel(doc.RootElement, options);
+ }
+
+ SimpleStandardModel IModelSerializable.Deserialize(BinaryData data, ModelSerializerOptions options)
+ {
+ PatchModelHelper.ValidateFormat(this, options.Format);
+
+ JsonDocument doc = JsonDocument.Parse(data);
+ return DeserializeSimpleStandardModel(doc.RootElement, options);
+ }
+
+ private void Serialize(Utf8JsonWriter writer, ModelSerializerOptions options)
+ {
+ writer.WriteStartObject();
+ if (Optional.IsDefined(Name))
+ {
+ writer.WritePropertyName("name"u8);
+ writer.WriteStringValue(Name);
+ }
+ if (Optional.IsDefined(Count))
+ {
+ writer.WritePropertyName("count"u8);
+ writer.WriteNumberValue(Count.Value);
+ }
+ if (Optional.IsDefined(UpdatedOn))
+ {
+ writer.WritePropertyName("updatedOn"u8);
+ writer.WriteStringValue(UpdatedOn.Value.UtcDateTime.ToString("O"));
+ }
+ writer.WriteEndObject();
+ }
+
+ void IModelJsonSerializable.Serialize(Utf8JsonWriter writer, ModelSerializerOptions options)
+ {
+ PatchModelHelper.ValidateFormat(this, options.Format);
+
+ Serialize(writer, options);
+ }
+
+ BinaryData IModelSerializable.Serialize(ModelSerializerOptions options)
+ {
+ PatchModelHelper.ValidateFormat(this, options.Format);
+
+ return ModelSerializer.SerializeCore(this, options);
+ }
+
+ public static implicit operator RequestContent(SimpleStandardModel model)
+ => RequestContent.Create(model, ModelSerializerOptions.DefaultWireOptions);
+
+ public static explicit operator SimpleStandardModel(Response response)
+ {
+ Argument.AssertNotNull(response, nameof(response));
+
+ using JsonDocument jsonDocument = JsonDocument.Parse(response.ContentStream);
+ return DeserializeSimpleStandardModel(jsonDocument.RootElement, ModelSerializerOptions.DefaultWireOptions);
+ }
+
+ void IUtf8JsonSerializable.Write(Utf8JsonWriter writer) => ((IModelJsonSerializable)this).Serialize(writer, ModelSerializerOptions.DefaultWireOptions);
+
+ // TODO: Temp for pef tests
+ public void Serialize(Utf8JsonWriter writer) => ((IUtf8JsonSerializable)this).Write(writer);
+ }
+}
diff --git a/sdk/core/Azure.Core/tests/public/PatchModels/SimpleStandardModel.cs b/sdk/core/Azure.Core/tests/public/PatchModels/SimpleStandardModel.cs
new file mode 100644
index 000000000000..e374790584ed
--- /dev/null
+++ b/sdk/core/Azure.Core/tests/public/PatchModels/SimpleStandardModel.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+
+namespace Azure.Core.Tests.PatchModels
+{
+ public partial class SimpleStandardModel
+ {
+ public SimpleStandardModel() { }
+
+ internal SimpleStandardModel(string name, int count, DateTimeOffset updatedOn)
+ {
+ Name = name;
+ Count = count;
+ UpdatedOn = updatedOn;
+ }
+
+ public string Name { get; set; }
+
+ public int? Count { get; set; }
+
+ public DateTimeOffset? UpdatedOn { get; set; }
+ }
+}