diff --git a/sdk/core/Azure.Core/api/Azure.Core.net461.cs b/sdk/core/Azure.Core/api/Azure.Core.net461.cs index 09f8421a1b0d..fda9495c23be 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net461.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net461.cs @@ -1110,6 +1110,7 @@ public SerializableOptions() { } public bool IgnoreAdditionalProperties { get { throw null; } set { } } public bool IgnoreReadOnlyProperties { get { throw null; } set { } } public bool PrettyPrint { get { throw null; } set { } } + public Azure.Core.Serialization.ObjectSerializer? Serializer { get { throw null; } set { } } } } namespace Azure.Messaging diff --git a/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs b/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs index e6a9068c396c..495c8e16746f 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs @@ -1110,6 +1110,7 @@ public SerializableOptions() { } public bool IgnoreAdditionalProperties { get { throw null; } set { } } public bool IgnoreReadOnlyProperties { get { throw null; } set { } } public bool PrettyPrint { get { throw null; } set { } } + public Azure.Core.Serialization.ObjectSerializer? Serializer { get { throw null; } set { } } } } namespace Azure.Messaging diff --git a/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs b/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs index e6a9068c396c..495c8e16746f 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs @@ -1110,6 +1110,7 @@ public SerializableOptions() { } public bool IgnoreAdditionalProperties { get { throw null; } set { } } public bool IgnoreReadOnlyProperties { get { throw null; } set { } } public bool PrettyPrint { get { throw null; } set { } } + public Azure.Core.Serialization.ObjectSerializer? Serializer { get { throw null; } set { } } } } namespace Azure.Messaging diff --git a/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs b/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs index 09f8421a1b0d..fda9495c23be 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs @@ -1110,6 +1110,7 @@ public SerializableOptions() { } public bool IgnoreAdditionalProperties { get { throw null; } set { } } public bool IgnoreReadOnlyProperties { get { throw null; } set { } } public bool PrettyPrint { get { throw null; } set { } } + public Azure.Core.Serialization.ObjectSerializer? Serializer { get { throw null; } set { } } } } namespace Azure.Messaging diff --git a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs index 09f8421a1b0d..fda9495c23be 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs @@ -1110,6 +1110,7 @@ public SerializableOptions() { } public bool IgnoreAdditionalProperties { get { throw null; } set { } } public bool IgnoreReadOnlyProperties { get { throw null; } set { } } public bool PrettyPrint { get { throw null; } set { } } + public Azure.Core.Serialization.ObjectSerializer? Serializer { get { throw null; } set { } } } } namespace Azure.Messaging diff --git a/sdk/core/Azure.Core/samples/Serialization.md b/sdk/core/Azure.Core/samples/Serialization.md index ec6de802c393..5634dbc96681 100644 --- a/sdk/core/Azure.Core/samples/Serialization.md +++ b/sdk/core/Azure.Core/samples/Serialization.md @@ -99,11 +99,8 @@ DogListProperty dog = new DogListProperty FoodConsumed = { "kibble", "egg", "peanut butter" }, }; -//stj example +//STJ example string json = JsonSerializer.Serialize(dog); - -//modelSerializer example -Stream stream = ModelSerializer.Serialize(dog); ``` Deserialization @@ -113,9 +110,6 @@ string json = "{\"latinName\":\"Animalia\",\"weight\":1.1,\"name\":\"Doggo\",\"i //stj example DogListProperty dog = JsonSerializer.Deserialize(json); - -//modelSerializer example -DogListProperty dog2 = ModelSerializer.Deserialize(json); ``` ## Using static deserializer @@ -130,4 +124,55 @@ string serviceResponse = Animal model = Animal.StaticDeserialize(new MemoryStream(Encoding.UTF8.GetBytes(serviceResponse)), options: options); ``` +## Using ModelSerializer + +Serialize would use the Try/Do examples from above. We would use Interface form the Serializable but potentially have static method for Deserialize. +When using Static Deserialize, an empty Model does not have to be created first as we can deserialize directly into a new instance. +Serialization +```C# Snippet:ModelSerializer_Serialize +DogListProperty dog = new DogListProperty +{ + Name = "Doggo", + IsHungry = true, + Weight = 1.1, + FoodConsumed = { "kibble", "egg", "peanut butter" }, +}; + +Stream stream = ModelSerializer.Serialize(dog); +``` + +Deserialization +```C# Snippet:ModelSerializer_Deserialize +string json = @"[{""LatinName"":""Animalia"",""Weight"":1.1,""Name"":""Doggo"",""IsHungry"":false,""FoodConsumed"":[""kibble"",""egg"",""peanut butter""],""NumberOfLegs"":4}]"; + +DogListProperty dog = ModelSerializer.Deserialize(json); +``` + +## Using ModelSerializer for NewtonSoftJson +By using the ModelSerializer class, a new instance of Dog does not need to be created before calling Deserialize. Also added ObjectSerializer to Options class so different kinds of Serializers can be used. + +Serialization +```C# Snippet:NewtonSoft_Serialize +DogListProperty dog = new DogListProperty +{ + Name = "Doggo", + IsHungry = true, + Weight = 1.1, + FoodConsumed = { "kibble", "egg", "peanut butter" }, +}; +SerializableOptions options = new SerializableOptions(); +options.Serializer = new NewtonsoftJsonObjectSerializer(); + +Stream stream = ModelSerializer.Serialize(dog, options); +``` + +Deserialization + +```C# Snippet:NewtonSoft_Deserialize +SerializableOptions options = new SerializableOptions(); +options.Serializer = new NewtonsoftJsonObjectSerializer(); +string json = @"[{""LatinName"":""Animalia"",""Weight"":1.1,""Name"":""Doggo"",""IsHungry"":false,""FoodConsumed"":[""kibble"",""egg"",""peanut butter""],""NumberOfLegs"":4}]"; + +DogListProperty dog = ModelSerializer.Deserialize(json, options); +``` diff --git a/sdk/core/Azure.Core/src/Serialization/ModelSerializer.cs b/sdk/core/Azure.Core/src/Serialization/ModelSerializer.cs index b6e780335cfb..391d64a566fe 100644 --- a/sdk/core/Azure.Core/src/Serialization/ModelSerializer.cs +++ b/sdk/core/Azure.Core/src/Serialization/ModelSerializer.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using System.IO; namespace Azure.Core.Serialization @@ -11,7 +12,7 @@ namespace Azure.Core.Serialization public static class ModelSerializer { /// - /// Serailize a model. + /// Serialize a model. /// /// /// @@ -19,6 +20,13 @@ public static class ModelSerializer /// public static Stream Serialize(T model, SerializableOptions? options = default) where T : IJsonSerializable, new() { + // if options.Serializer is set + if (options != null && options.Serializer != null) + { + System.BinaryData data = options.Serializer.Serialize(model); + return data.ToStream(); + } + IJsonSerializable serializable = (model ??= new T()) as IJsonSerializable; Stream stream = new MemoryStream(); serializable.Serialize(stream, options); @@ -34,6 +42,15 @@ public static class ModelSerializer /// public static T Deserialize(Stream stream, SerializableOptions? options = default) where T : IJsonSerializable, new() { + if (options != null && options.Serializer != null) + { + var obj = options.Serializer.Deserialize(stream, typeof(T), default); + if (obj is null) + throw new InvalidOperationException(); + else + return (T)obj; + } + IJsonSerializable serializable = new T(); serializable.Deserialize(stream, options); return (T)serializable; @@ -53,6 +70,15 @@ public static class ModelSerializer writer.Write(json); stream.Position = 0; + if (options != null && options.Serializer != null) + { + var obj = options.Serializer.Deserialize(stream, typeof(T), default); + if (obj is null) + throw new InvalidOperationException(); + else + return (T)obj; + } + IJsonSerializable serializable = new T(); serializable.Deserialize(stream, options); return (T)serializable; diff --git a/sdk/core/Azure.Core/src/Serialization/SerializableOptions.cs b/sdk/core/Azure.Core/src/Serialization/SerializableOptions.cs index 71e274375b3a..a9c201ced116 100644 --- a/sdk/core/Azure.Core/src/Serialization/SerializableOptions.cs +++ b/sdk/core/Azure.Core/src/Serialization/SerializableOptions.cs @@ -22,5 +22,10 @@ public class SerializableOptions /// Bool that determines if Json will be PrettyPrinted. Default is false. /// public bool PrettyPrint { get; set; } + + /// + /// todo + /// + public ObjectSerializer? Serializer { get; set; } } } diff --git a/sdk/core/Azure.Core/tests/ModelSerializationTests/NewtonSoftTests.cs b/sdk/core/Azure.Core/tests/ModelSerializationTests/NewtonSoftTests.cs new file mode 100644 index 000000000000..957080bf0095 --- /dev/null +++ b/sdk/core/Azure.Core/tests/ModelSerializationTests/NewtonSoftTests.cs @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Azure.Core.Serialization; +using NUnit.Framework; +using Newtonsoft.Json; + +namespace Azure.Core.Tests.ModelSerializationTests +{ + internal class NewtonSoftTests + { + private readonly SerializableOptions _wireOptions = new SerializableOptions { IgnoreReadOnlyProperties = false }; + private readonly SerializableOptions _objectOptions = new SerializableOptions(); + + [TestCase(true)] + [TestCase(false)] + public void CanRoundTripFutureVersionWithoutLoss(bool ignoreReadOnly) + { + Stream stream = new MemoryStream(); + string serviceResponse = + "{\"latinName\":\"Animalia\",\"weight\":2.3,\"name\":\"Rabbit\",\"isHungry\":false}"; + + StringBuilder expectedSerialized = new StringBuilder("{"); + expectedSerialized.Append("\"IsHungry\":false,"); + expectedSerialized.Append("\"Weight\":2.3,"); + if (!ignoreReadOnly) + { + expectedSerialized.Append("\"LatinName\":\"Animalia\","); + } + expectedSerialized.Append("\"Name\":\"Rabbit\""); + expectedSerialized.Append("}"); + var expectedSerializedString = expectedSerialized.ToString(); + + SerializableOptions options = new SerializableOptions() { IgnoreReadOnlyProperties = ignoreReadOnly }; + + if (ignoreReadOnly) + { + JsonSerializerSettings settings = new JsonSerializerSettings + { + ContractResolver = new IgnoreReadOnlyPropertiesResolver() + }; + options.Serializer = new NewtonsoftJsonObjectSerializer(settings); + } + else + options.Serializer = new NewtonsoftJsonObjectSerializer(); + + var model = ModelSerializer.Deserialize(new MemoryStream(Encoding.UTF8.GetBytes(serviceResponse)), options: options); + + if (!ignoreReadOnly) + { + Assert.That(model.LatinName, Is.EqualTo("Animalia")); + } + Assert.That(model.Name, Is.EqualTo("Rabbit")); + Assert.IsFalse(model.IsHungry); + +#if NET6_0_OR_GREATER + Assert.That(model.Weight, Is.EqualTo(2.3)); +#endif + + stream = ModelSerializer.Serialize(model, options); + stream.Position = 0; + string roundTrip = new StreamReader(stream).ReadToEnd(); + +#if NET6_0_OR_GREATER + Assert.That(roundTrip, Is.EqualTo(expectedSerializedString)); +#endif + + var model2 = ModelSerializer.Deserialize(new MemoryStream(Encoding.UTF8.GetBytes(roundTrip)), options: options); + VerifyModels.CheckAnimals(model, model2, options); + } + + // Generate a class that implements the NewtonSoft default contract resolver so that ReadOnly properties are not serialized + // This is used to verify that the ReadOnly properties are not serialized when IgnoreReadOnlyProperties is set to true + private class IgnoreReadOnlyPropertiesResolver : Newtonsoft.Json.Serialization.DefaultContractResolver + { + protected override Newtonsoft.Json.Serialization.JsonProperty CreateProperty(System.Reflection.MemberInfo member, MemberSerialization memberSerialization) + { + Newtonsoft.Json.Serialization.JsonProperty property = base.CreateProperty(member, memberSerialization); + + if (!property.Writable) + { + property.ShouldSerialize = obj => false; + } + + return property; + } + } + } +} diff --git a/sdk/core/Azure.Core/tests/samples/SerializationSamples.cs b/sdk/core/Azure.Core/tests/samples/SerializationSamples.cs index 89121197a2fe..583f898c3191 100644 --- a/sdk/core/Azure.Core/tests/samples/SerializationSamples.cs +++ b/sdk/core/Azure.Core/tests/samples/SerializationSamples.cs @@ -2,18 +2,13 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text; using System.Text.Json; -using System.Threading.Tasks; using Azure.Core.Experimental.Tests; using Azure.Core.Serialization; using Azure.Core.TestFramework; using Azure.Core.Tests.ModelSerializationTests; -using Azure.Identity; -using Azure.Security.KeyVault.Secrets; using NUnit.Framework; namespace Azure.Core.Samples @@ -116,11 +111,8 @@ public void StjSerialize() FoodConsumed = { "kibble", "egg", "peanut butter" }, }; - //stj example + //STJ example string json = JsonSerializer.Serialize(dog); - - //modelSerializer example - Stream stream = ModelSerializer.Serialize(dog); #endregion } @@ -133,9 +125,6 @@ public void StjDeserialize() //stj example DogListProperty dog = JsonSerializer.Deserialize(json); - - //modelSerializer example - DogListProperty dog2 = ModelSerializer.Deserialize(json); #endregion } @@ -151,5 +140,65 @@ public void StaticDeserialize() Animal model = Animal.StaticDeserialize(new MemoryStream(Encoding.UTF8.GetBytes(serviceResponse)), options: options); #endregion } + + [Test] + [Ignore("Only verifying that the sample builds")] + public void NewtonSoftSerialize() + { + #region Snippet:NewtonSoft_Serialize + DogListProperty dog = new DogListProperty + { + Name = "Doggo", + IsHungry = true, + Weight = 1.1, + FoodConsumed = { "kibble", "egg", "peanut butter" }, + }; + SerializableOptions options = new SerializableOptions(); + options.Serializer = new NewtonsoftJsonObjectSerializer(); + + Stream stream = ModelSerializer.Serialize(dog, options); + #endregion + } + + [Test] + [Ignore("Only verifying that the sample builds")] + public void NewtonSoftDeserialize() + { + #region Snippet:NewtonSoft_Deserialize + SerializableOptions options = new SerializableOptions(); + options.Serializer = new NewtonsoftJsonObjectSerializer(); + string json = @"[{""LatinName"":""Animalia"",""Weight"":1.1,""Name"":""Doggo"",""IsHungry"":false,""FoodConsumed"":[""kibble"",""egg"",""peanut butter""],""NumberOfLegs"":4}]"; + + DogListProperty dog = ModelSerializer.Deserialize(json, options); + #endregion + } + + [Test] + [Ignore("Only verifying that the sample builds")] + public void ModelSerializerSerialize() + { + #region Snippet:ModelSerializer_Serialize + DogListProperty dog = new DogListProperty + { + Name = "Doggo", + IsHungry = true, + Weight = 1.1, + FoodConsumed = { "kibble", "egg", "peanut butter" }, + }; + + Stream stream = ModelSerializer.Serialize(dog); + #endregion + } + + [Test] + [Ignore("Only verifying that the sample builds")] + public void ModelSerializerDeserialize() + { + #region Snippet:ModelSerializer_Deserialize + string json = @"[{""LatinName"":""Animalia"",""Weight"":1.1,""Name"":""Doggo"",""IsHungry"":false,""FoodConsumed"":[""kibble"",""egg"",""peanut butter""],""NumberOfLegs"":4}]"; + + DogListProperty dog = ModelSerializer.Deserialize(json); + #endregion + } } }