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; } + } +}