diff --git a/sdk/core/Azure.Core/api/Azure.Core.net461.cs b/sdk/core/Azure.Core/api/Azure.Core.net461.cs index 4ccc966869bb..a3218d50be79 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net461.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net461.cs @@ -89,6 +89,11 @@ public HttpAuthorization(string scheme, string parameter) { } public static bool operator !=(Azure.HttpRange left, Azure.HttpRange right) { throw null; } public override string ToString() { throw null; } } + public partial interface IJsonSerializable + { + bool TryDeserialize(System.IO.Stream stream, out long bytesConsumed, Azure.SerializableOptions? options = null); + bool TrySerialize(System.IO.Stream stream, out long bytesWritten, Azure.SerializableOptions? options = null); + } public partial class JsonPatchDocument { public JsonPatchDocument() { } @@ -262,6 +267,13 @@ protected Response() { } public override int GetHashCode() { throw null; } public static implicit operator T (Azure.Response response) { throw null; } } + public partial class SerializableOptions + { + public SerializableOptions() { } + public bool HandleAdditionalProperties { get { throw null; } set { } } + public bool IncludeReadOnlyProperties { get { throw null; } set { } } + public bool PrettyPrint { get { throw null; } set { } } + } public partial class SyncAsyncEventArgs : System.EventArgs { public SyncAsyncEventArgs(bool isRunningSynchronously, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { } 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 0d35cd47af87..d135442d4127 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net5.0.cs @@ -89,6 +89,11 @@ public HttpAuthorization(string scheme, string parameter) { } public static bool operator !=(Azure.HttpRange left, Azure.HttpRange right) { throw null; } public override string ToString() { throw null; } } + public partial interface IJsonSerializable + { + bool TryDeserialize(System.IO.Stream stream, out long bytesConsumed, Azure.SerializableOptions? options = null); + bool TrySerialize(System.IO.Stream stream, out long bytesWritten, Azure.SerializableOptions? options = null); + } public partial class JsonPatchDocument { public JsonPatchDocument() { } @@ -262,6 +267,13 @@ protected Response() { } public override int GetHashCode() { throw null; } public static implicit operator T (Azure.Response response) { throw null; } } + public partial class SerializableOptions + { + public SerializableOptions() { } + public bool HandleAdditionalProperties { get { throw null; } set { } } + public bool IncludeReadOnlyProperties { get { throw null; } set { } } + public bool PrettyPrint { get { throw null; } set { } } + } public partial class SyncAsyncEventArgs : System.EventArgs { public SyncAsyncEventArgs(bool isRunningSynchronously, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { } 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 0d35cd47af87..d135442d4127 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.net6.0.cs @@ -89,6 +89,11 @@ public HttpAuthorization(string scheme, string parameter) { } public static bool operator !=(Azure.HttpRange left, Azure.HttpRange right) { throw null; } public override string ToString() { throw null; } } + public partial interface IJsonSerializable + { + bool TryDeserialize(System.IO.Stream stream, out long bytesConsumed, Azure.SerializableOptions? options = null); + bool TrySerialize(System.IO.Stream stream, out long bytesWritten, Azure.SerializableOptions? options = null); + } public partial class JsonPatchDocument { public JsonPatchDocument() { } @@ -262,6 +267,13 @@ protected Response() { } public override int GetHashCode() { throw null; } public static implicit operator T (Azure.Response response) { throw null; } } + public partial class SerializableOptions + { + public SerializableOptions() { } + public bool HandleAdditionalProperties { get { throw null; } set { } } + public bool IncludeReadOnlyProperties { get { throw null; } set { } } + public bool PrettyPrint { get { throw null; } set { } } + } public partial class SyncAsyncEventArgs : System.EventArgs { public SyncAsyncEventArgs(bool isRunningSynchronously, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { } 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 4ccc966869bb..a3218d50be79 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netcoreapp2.1.cs @@ -89,6 +89,11 @@ public HttpAuthorization(string scheme, string parameter) { } public static bool operator !=(Azure.HttpRange left, Azure.HttpRange right) { throw null; } public override string ToString() { throw null; } } + public partial interface IJsonSerializable + { + bool TryDeserialize(System.IO.Stream stream, out long bytesConsumed, Azure.SerializableOptions? options = null); + bool TrySerialize(System.IO.Stream stream, out long bytesWritten, Azure.SerializableOptions? options = null); + } public partial class JsonPatchDocument { public JsonPatchDocument() { } @@ -262,6 +267,13 @@ protected Response() { } public override int GetHashCode() { throw null; } public static implicit operator T (Azure.Response response) { throw null; } } + public partial class SerializableOptions + { + public SerializableOptions() { } + public bool HandleAdditionalProperties { get { throw null; } set { } } + public bool IncludeReadOnlyProperties { get { throw null; } set { } } + public bool PrettyPrint { get { throw null; } set { } } + } public partial class SyncAsyncEventArgs : System.EventArgs { public SyncAsyncEventArgs(bool isRunningSynchronously, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { } 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 4ccc966869bb..a3218d50be79 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs @@ -89,6 +89,11 @@ public HttpAuthorization(string scheme, string parameter) { } public static bool operator !=(Azure.HttpRange left, Azure.HttpRange right) { throw null; } public override string ToString() { throw null; } } + public partial interface IJsonSerializable + { + bool TryDeserialize(System.IO.Stream stream, out long bytesConsumed, Azure.SerializableOptions? options = null); + bool TrySerialize(System.IO.Stream stream, out long bytesWritten, Azure.SerializableOptions? options = null); + } public partial class JsonPatchDocument { public JsonPatchDocument() { } @@ -262,6 +267,13 @@ protected Response() { } public override int GetHashCode() { throw null; } public static implicit operator T (Azure.Response response) { throw null; } } + public partial class SerializableOptions + { + public SerializableOptions() { } + public bool HandleAdditionalProperties { get { throw null; } set { } } + public bool IncludeReadOnlyProperties { get { throw null; } set { } } + public bool PrettyPrint { get { throw null; } set { } } + } public partial class SyncAsyncEventArgs : System.EventArgs { public SyncAsyncEventArgs(bool isRunningSynchronously, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { } diff --git a/sdk/core/Azure.Core/tests/ModelSerializationTests/Animal.cs b/sdk/core/Azure.Core/tests/ModelSerializationTests/Animal.cs index 11c206d91039..907e2918fe1d 100644 --- a/sdk/core/Azure.Core/tests/ModelSerializationTests/Animal.cs +++ b/sdk/core/Azure.Core/tests/ModelSerializationTests/Animal.cs @@ -13,7 +13,7 @@ public class Animal : IJsonSerializable, IUtf8JsonSerializable private Dictionary RawData { get; set; } = new Dictionary(); public bool IsHungry { get; set; } = false; - public double Weight { get; set; } = 0; + public double Weight { get; set; } = 1.1; public string LatinName { get; private set; } = "Animalia"; public string Name { get; set; } = "Animal"; @@ -38,6 +38,11 @@ internal Animal(double weight, string latinName, string name, bool isHungry, Dic RawData = rawData; } + internal Animal(string name) + { + Name = name; + } + #region Serialization void IUtf8JsonSerializable.Write(Utf8JsonWriter writer, SerializableOptions options) { diff --git a/sdk/core/Azure.Core/tests/ModelSerializationTests/CatReadOnlyProperty.cs b/sdk/core/Azure.Core/tests/ModelSerializationTests/CatReadOnlyProperty.cs index 2b5a63b82c94..4f7152db6921 100644 --- a/sdk/core/Azure.Core/tests/ModelSerializationTests/CatReadOnlyProperty.cs +++ b/sdk/core/Azure.Core/tests/ModelSerializationTests/CatReadOnlyProperty.cs @@ -14,10 +14,6 @@ public class CatReadOnlyProperty : Animal, IJsonSerializable, IUtf8JsonSerializa public CatReadOnlyProperty(double weight, string latinName, string name, bool isHungry, bool hasWhiskers) : base(weight, "Felis catus", name, isHungry) { - CatLatinName = LatinName; - CatIsHungry = IsHungry; - CatWeight = Weight; - CatName = Name; HasWhiskers = hasWhiskers; } @@ -28,11 +24,6 @@ internal CatReadOnlyProperty(double weight, string latinName, string name, bool public bool HasWhiskers { get; set; } = true; - private string CatLatinName; - private bool CatIsHungry; - private double CatWeight; - private string CatName; - #region Serialization void IUtf8JsonSerializable.Write(Utf8JsonWriter writer, SerializableOptions options) { @@ -43,11 +34,11 @@ void IUtf8JsonSerializable.Write(Utf8JsonWriter writer, SerializableOptions opti writer.WriteStringValue(LatinName); } writer.WritePropertyName("name"u8); - writer.WriteStringValue(CatName); + writer.WriteStringValue(Name); writer.WritePropertyName("isHungry"u8); - writer.WriteBooleanValue(CatIsHungry); + writer.WriteBooleanValue(IsHungry); writer.WritePropertyName("weight"u8); - writer.WriteNumberValue(CatWeight); + writer.WriteNumberValue(Weight); writer.WritePropertyName("hasWhiskers"u8); writer.WriteBooleanValue(HasWhiskers); @@ -122,11 +113,10 @@ internal static CatReadOnlyProperty DeserializeCatReadOnlyProperty(JsonElement e { JsonDocument jsonDocument = JsonDocument.Parse(stream); var model = DeserializeCatReadOnlyProperty(jsonDocument.RootElement, options ?? new SerializableOptions()); - this.CatLatinName = model.LatinName; - this.CatWeight = model.Weight; - this.CatIsHungry = model.IsHungry; + this.Weight = model.Weight; + this.IsHungry = model.IsHungry; this.HasWhiskers = model.HasWhiskers; - this.CatIsHungry = model.CatIsHungry; + this.IsHungry = model.IsHungry; bytesConsumed = stream.Length; return true; } diff --git a/sdk/core/Azure.Core/tests/ModelSerializationTests/DogListProperty.cs b/sdk/core/Azure.Core/tests/ModelSerializationTests/DogListProperty.cs new file mode 100644 index 000000000000..6dfae6392b9b --- /dev/null +++ b/sdk/core/Azure.Core/tests/ModelSerializationTests/DogListProperty.cs @@ -0,0 +1,165 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; + +namespace Azure.Core.Tests.ModelSerializationTests +{ + public class DogListProperty : Animal, IJsonSerializable, IUtf8JsonSerializable + { + private Dictionary RawData { get; set; } = new Dictionary(); + public List FoodConsumed { get; set; } = new List {"kibble", "egg", "peanut butter"}; + + public DogListProperty(string name) : base(name) + { + Name = name; + } + + internal DogListProperty(double weight, string latinName, string name, bool isHungry, List foodConsumed, Dictionary rawData) : base(weight, latinName, name, isHungry, rawData) + { + RawData = rawData; + FoodConsumed = foodConsumed; + } + + public DogListProperty() + { + } + + #region Serialization + void IUtf8JsonSerializable.Write(Utf8JsonWriter writer, SerializableOptions options) + { + writer.WriteStartObject(); + if (options.IncludeReadOnlyProperties) + { + writer.WritePropertyName("latinName"u8); + writer.WriteStringValue(LatinName); + } + writer.WritePropertyName("name"u8); + writer.WriteStringValue(Name); + writer.WritePropertyName("isHungry"u8); + writer.WriteBooleanValue(IsHungry); + writer.WritePropertyName("weight"u8); + writer.WriteNumberValue(Weight); + + writer.WritePropertyName("foodConsumed"u8); + writer.WriteStartArray(); + foreach (var item in FoodConsumed) + { + writer.WriteStringValue($"{item}"); + } + writer.WriteEndArray(); + + if (options.HandleAdditionalProperties) + { + //write out the raw data + foreach (var property in RawData) + { + writer.WritePropertyName(property.Key); +#if NET6_0_OR_GREATER + writer.WriteRawValue(property.Value); +#else + JsonSerializer.Serialize(writer, JsonDocument.Parse(property.Value.ToString()).RootElement); +#endif + } + } + writer.WriteEndObject(); + } + + internal static DogListProperty DeserializeDogListProperty(JsonElement element, SerializableOptions options) + { + double weight = default; + string name = ""; + string latinName = ""; + bool isHungry = default; + Dictionary rawData = new Dictionary(); + List foodConsumed = new List(); + + foreach (var property in element.EnumerateObject()) + { + if (property.NameEquals("weight"u8)) + { + weight = property.Value.GetDouble(); + continue; + } + if (property.NameEquals("name"u8)) + { + name = property.Value.GetString(); + continue; + } + if (property.NameEquals("latinName"u8)) + { + latinName = property.Value.GetString(); + continue; + } + if (property.NameEquals("isHungry"u8)) + { + isHungry = property.Value.GetBoolean(); + continue; + } + if (property.NameEquals("foodConsumed"u8)) + { + foreach (var item in property.Value.EnumerateArray()) + { + foodConsumed.Add(item.GetString()); + } + continue; + } + if (options.HandleAdditionalProperties) + { + //this means its an unknown property we got + rawData.Add(property.Name, BinaryData.FromString(property.Value.GetRawText())); + } + } + return new DogListProperty(weight, latinName, name, isHungry, foodConsumed, rawData); + } + #endregion + + #region InterfaceImplementation + public new bool TryDeserialize(Stream stream, out long bytesConsumed, SerializableOptions options = default) + { + bytesConsumed = 0; + try + { + JsonDocument jsonDocument = JsonDocument.Parse(stream); + var model = DeserializeDogListProperty(jsonDocument.RootElement, options ?? new SerializableOptions()); + this.Name = model.Name; + this.Weight = model.Weight; + this.IsHungry = model.IsHungry; + this.FoodConsumed = model.FoodConsumed; + this.RawData = model.RawData; + bytesConsumed = stream.Length; + return true; + } + catch + { + return false; + } + } + + public new bool TrySerialize(Stream stream, out long bytesWritten, SerializableOptions options = default) + { + bytesWritten = 0; + try + { + JsonWriterOptions jsonWriterOptions = new JsonWriterOptions(); + if (options.PrettyPrint) + { + jsonWriterOptions.Indented = true; + } + Utf8JsonWriter writer = new Utf8JsonWriter(stream, jsonWriterOptions); + ((IUtf8JsonSerializable)this).Write(writer, options ?? new SerializableOptions()); + writer.Flush(); + bytesWritten = (int)stream.Length; + return true; + } + catch + { + return false; + } + } + #endregion + } +} diff --git a/sdk/core/Azure.Core/tests/ModelSerializationTests/ListPropertyTests.cs b/sdk/core/Azure.Core/tests/ModelSerializationTests/ListPropertyTests.cs new file mode 100644 index 000000000000..9b17adad84b0 --- /dev/null +++ b/sdk/core/Azure.Core/tests/ModelSerializationTests/ListPropertyTests.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using Azure; +using Azure.Core.Tests; +using Azure.Core.Tests.ModelSerializationTests; +using NUnit.Framework; + +namespace Azure.Core.Tests.ModelSerializationTests +{ + public class ListPropertyTests + { + private readonly SerializableOptions _wireOptions = new SerializableOptions { IncludeReadOnlyProperties = false }; + private readonly SerializableOptions _objectOptions = new SerializableOptions(); + + [TestCase(true, true)] + [TestCase(true, false)] + [TestCase(false, true)] + [TestCase(false, false)] + public void CanRoundTripFutureVersionWithoutLoss(bool includeReadonly, bool handleUnknown) + { + Stream stream = new MemoryStream(); + string serviceResponse = + "{\"latinName\":\"Animalia\",\"weight\":1.1,\"name\":\"Doggo\",\"isHungry\":false,\"foodConsumed\":[\"kibble\",\"egg\",\"peanut butter\"], \"numberOfLegs\":4}"; + + StringBuilder expectedSerialized = new StringBuilder("{"); + if (includeReadonly) + { + expectedSerialized.Append("\"latinName\":\"Animalia\","); + } + expectedSerialized.Append("\"name\":\"Doggo\","); + expectedSerialized.Append("\"isHungry\":false,"); + expectedSerialized.Append("\"weight\":1.1,"); + expectedSerialized.Append("\"foodConsumed\":[\"kibble\",\"egg\",\"peanut butter\"]"); + if (handleUnknown) + { + expectedSerialized.Append(",\"numberOfLegs\":4"); + } + expectedSerialized.Append("}"); + var expectedSerializedString = expectedSerialized.ToString(); + + SerializableOptions options = new SerializableOptions() { IncludeReadOnlyProperties = includeReadonly, HandleAdditionalProperties = handleUnknown }; + + var model = new DogListProperty(); + model.TryDeserialize(new MemoryStream(Encoding.UTF8.GetBytes(serviceResponse)), out long bytesConsumed, options: options); + + if (includeReadonly) + { + Assert.That(model.LatinName, Is.EqualTo("Animalia")); + } + Assert.That(model.Name, Is.EqualTo("Doggo")); + Assert.IsFalse(model.IsHungry); + Assert.That(model.Weight, Is.EqualTo(1.1)); + Assert.That(model.FoodConsumed, Is.EqualTo(new List { "kibble", "egg", "peanut butter" })); + + if (handleUnknown) + { + var additionalProperties = typeof(DogListProperty).GetProperty("RawData", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).GetValue(model) as Dictionary; + Assert.AreEqual(1, additionalProperties.Count); + Assert.IsTrue(additionalProperties.ContainsKey("numberOfLegs")); + Assert.IsTrue(additionalProperties["numberOfLegs"].ToString() == "4"); + } + Assert.That(serviceResponse.Length, Is.EqualTo(bytesConsumed)); + model.TrySerialize(stream, out var bytesWritten, options: options); + stream.Position = 0; + string roundTrip = new StreamReader(stream).ReadToEnd(); + Assert.That(roundTrip, Is.EqualTo(expectedSerializedString)); + Assert.That(expectedSerialized.Length, Is.EqualTo(bytesWritten)); + + var model2 = new DogListProperty(); + model2.TryDeserialize(new MemoryStream(Encoding.UTF8.GetBytes(roundTrip)), out bytesConsumed, options: options); + + if (includeReadonly) + Assert.That(model.LatinName, Is.EqualTo(model2.LatinName)); + Assert.That(model.Name, Is.EqualTo(model2.Name)); + Assert.That(model.Weight, Is.EqualTo(model2.Weight)); + Assert.That(roundTrip.Length, Is.EqualTo(bytesConsumed)); + Assert.That(model.FoodConsumed, Is.EqualTo(model2.FoodConsumed)); + if (handleUnknown) + { + var additionalProperties1 = typeof(DogListProperty).GetProperty("RawData", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).GetValue(model) as Dictionary; + var additionalProperties2 = typeof(DogListProperty).GetProperty("RawData", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic).GetValue(model2) as Dictionary; + + Assert.AreEqual(1, additionalProperties1.Count); + Assert.IsTrue(additionalProperties1.ContainsKey("numberOfLegs")); + Assert.IsTrue(additionalProperties1["numberOfLegs"].ToString() == "4"); + Assert.AreEqual(1, additionalProperties2.Count); + Assert.IsTrue(additionalProperties2.ContainsKey("numberOfLegs")); + Assert.IsTrue(additionalProperties2["numberOfLegs"].ToString() == "4"); + } + } + + [Test] + public void PrettyPrint() + { + DogListProperty model = new DogListProperty("Doggo"); + + Stream stream = new MemoryStream(); + model.TrySerialize(stream, out long bytesWritten, options: new SerializableOptions() { PrettyPrint = true }); + stream.Position = 0; + var actualJson = new StreamReader(stream).ReadToEnd(); + + var expectedJson = """ + { + "latinName": "Animalia", + "name": "Doggo", + "isHungry": false, + "weight": 1.1, + "foodConsumed": [ + "kibble", + "egg", + "peanut butter" + ] + } + """; + + Assert.AreEqual(expectedJson, actualJson); + } + } +} diff --git a/sdk/core/Azure.Core/tests/ModelSerializationTests/SerializableTests.cs b/sdk/core/Azure.Core/tests/ModelSerializationTests/ReadOnlyPropertyTests.cs similarity index 99% rename from sdk/core/Azure.Core/tests/ModelSerializationTests/SerializableTests.cs rename to sdk/core/Azure.Core/tests/ModelSerializationTests/ReadOnlyPropertyTests.cs index a2925873ea4f..5aba9069c5cb 100644 --- a/sdk/core/Azure.Core/tests/ModelSerializationTests/SerializableTests.cs +++ b/sdk/core/Azure.Core/tests/ModelSerializationTests/ReadOnlyPropertyTests.cs @@ -12,7 +12,7 @@ namespace Azure.Core.Tests.ModelSerializationTests { - public class SerializableTests + public class ReadOnlyPropertyTests { private readonly SerializableOptions _wireOptions = new SerializableOptions { IncludeReadOnlyProperties = false }; private readonly SerializableOptions _objectOptions = new SerializableOptions();