From c3e2a5712578bf9b35ceb916b8d79080edc560f8 Mon Sep 17 00:00:00 2001 From: Heath Stewart Date: Thu, 6 Aug 2020 14:27:52 -0700 Subject: [PATCH] Initial serialized member name mapping (#13527) * Initial implementation of prototype serialization types * Initial serialized member name mapping * Update APIs and READMEs * Fix README and add CHANGELOG * Resolves some PR feedback * Resolve PR feedback * Resolve archboard feedback * Update public API * Updated documentation and versions * Fix CHANGELOG entry --- sdk/core/Azure.Core/Azure.Core.sln | 26 +- sdk/core/Azure.Core/CHANGELOG.md | 5 + .../api/Azure.Core.netstandard2.0.cs | 7 +- .../src/Serialization/IMemberNameConverter.cs | 22 ++ .../src/Serialization/JsonObjectSerializer.cs | 81 +++++- .../src/Serialization/ObjectSerializer.cs | 2 +- .../tests/JsonObjectSerializerTest.cs | 135 ++++++++- .../CHANGELOG.md | 7 + .../Directory.Build.props | 7 + .../README.md | 49 ++++ ...zure.Core.NewtonsoftJson.netstandard2.0.cs | 13 + ...Microsoft.Azure.Core.NewtonsoftJson.csproj | 25 ++ .../NewtonsoftJsonObjectSerializer.cs | 108 +++++++ ...oft.Azure.Core.NewtonsoftJson.Tests.csproj | 17 ++ .../NewtonsoftJsonObjectSerializerTest.cs | 269 ++++++++++++++++++ sdk/core/ci.yml | 4 +- 16 files changed, 755 insertions(+), 22 deletions(-) create mode 100644 sdk/core/Azure.Core/src/Serialization/IMemberNameConverter.cs create mode 100644 sdk/core/Microsoft.Azure.Core.NewtonsoftJson/CHANGELOG.md create mode 100644 sdk/core/Microsoft.Azure.Core.NewtonsoftJson/Directory.Build.props create mode 100644 sdk/core/Microsoft.Azure.Core.NewtonsoftJson/README.md create mode 100644 sdk/core/Microsoft.Azure.Core.NewtonsoftJson/api/Microsoft.Azure.Core.NewtonsoftJson.netstandard2.0.cs create mode 100644 sdk/core/Microsoft.Azure.Core.NewtonsoftJson/src/Microsoft.Azure.Core.NewtonsoftJson.csproj create mode 100644 sdk/core/Microsoft.Azure.Core.NewtonsoftJson/src/Serialization/NewtonsoftJsonObjectSerializer.cs create mode 100644 sdk/core/Microsoft.Azure.Core.NewtonsoftJson/tests/Microsoft.Azure.Core.NewtonsoftJson.Tests.csproj create mode 100644 sdk/core/Microsoft.Azure.Core.NewtonsoftJson/tests/NewtonsoftJsonObjectSerializerTest.cs diff --git a/sdk/core/Azure.Core/Azure.Core.sln b/sdk/core/Azure.Core/Azure.Core.sln index 3f93d9944bff..5d4904082fff 100644 --- a/sdk/core/Azure.Core/Azure.Core.sln +++ b/sdk/core/Azure.Core/Azure.Core.sln @@ -11,7 +11,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Azure" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Azure.Tests", "..\Microsoft.Extensions.Azure\tests\Microsoft.Extensions.Azure.Tests.csproj", "{5B093527-8B04-4D2B-B23D-441D4B5EE305}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.Core.TestFramework", "..\Azure.Core.TestFramework\src\Azure.Core.TestFramework.csproj", "{EB0781C6-6C18-4C2A-8BDB-D61A1460AB09}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Core.TestFramework", "..\Azure.Core.TestFramework\src\Azure.Core.TestFramework.csproj", "{EB0781C6-6C18-4C2A-8BDB-D61A1460AB09}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Core.Experimental", "..\Azure.Core.Experimental\src\Azure.Core.Experimental.csproj", "{08159FC6-D991-4728-A12C-55A05C906465}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.Core.Experimental.Tests", "..\Azure.Core.Experimental\tests\Azure.Core.Experimental.Tests.csproj", "{2310EC12-5F3A-4328-9926-0212A22696F5}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.Core.NewtonsoftJson", "..\Microsoft.Azure.Core.NewtonsoftJson\src\Microsoft.Azure.Core.NewtonsoftJson.csproj", "{0CF90583-C79F-44F8-B81F-5558D90CF621}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Azure.Core.NewtonsoftJson.Tests", "..\Microsoft.Azure.Core.NewtonsoftJson\tests\Microsoft.Azure.Core.NewtonsoftJson.Tests.csproj", "{3F70FC18-5F83-4AEE-A9BD-AE100A0B1BF8}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -39,6 +47,22 @@ Global {EB0781C6-6C18-4C2A-8BDB-D61A1460AB09}.Debug|Any CPU.Build.0 = Debug|Any CPU {EB0781C6-6C18-4C2A-8BDB-D61A1460AB09}.Release|Any CPU.ActiveCfg = Release|Any CPU {EB0781C6-6C18-4C2A-8BDB-D61A1460AB09}.Release|Any CPU.Build.0 = Release|Any CPU + {08159FC6-D991-4728-A12C-55A05C906465}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {08159FC6-D991-4728-A12C-55A05C906465}.Debug|Any CPU.Build.0 = Debug|Any CPU + {08159FC6-D991-4728-A12C-55A05C906465}.Release|Any CPU.ActiveCfg = Release|Any CPU + {08159FC6-D991-4728-A12C-55A05C906465}.Release|Any CPU.Build.0 = Release|Any CPU + {2310EC12-5F3A-4328-9926-0212A22696F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2310EC12-5F3A-4328-9926-0212A22696F5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2310EC12-5F3A-4328-9926-0212A22696F5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2310EC12-5F3A-4328-9926-0212A22696F5}.Release|Any CPU.Build.0 = Release|Any CPU + {0CF90583-C79F-44F8-B81F-5558D90CF621}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0CF90583-C79F-44F8-B81F-5558D90CF621}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0CF90583-C79F-44F8-B81F-5558D90CF621}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0CF90583-C79F-44F8-B81F-5558D90CF621}.Release|Any CPU.Build.0 = Release|Any CPU + {3F70FC18-5F83-4AEE-A9BD-AE100A0B1BF8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3F70FC18-5F83-4AEE-A9BD-AE100A0B1BF8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3F70FC18-5F83-4AEE-A9BD-AE100A0B1BF8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3F70FC18-5F83-4AEE-A9BD-AE100A0B1BF8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/sdk/core/Azure.Core/CHANGELOG.md b/sdk/core/Azure.Core/CHANGELOG.md index 1b82914b5717..8c8ce90db78e 100644 --- a/sdk/core/Azure.Core/CHANGELOG.md +++ b/sdk/core/Azure.Core/CHANGELOG.md @@ -2,6 +2,11 @@ ## 1.4.0-preview.1 (Unreleased) +### Added +- Added `ObjectSerializer` base class for serialization. +- Added `IMemberNameConverter` for converting member names to serialized property names. +- Added `JsonObjectSerializer` that implements `ObjectSerializer` for `System.Text.Json`. + ### Fixed - Connection leak for retried non-buffered requests on .NET Framework. 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 4115f0cdc18b..c0b60287caf0 100644 --- a/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs +++ b/sdk/core/Azure.Core/api/Azure.Core.netstandard2.0.cs @@ -497,10 +497,15 @@ protected HttpPipelineTransport() { } } namespace Azure.Core.Serialization { - public partial class JsonObjectSerializer : Azure.Core.Serialization.ObjectSerializer + public partial interface IMemberNameConverter + { + string? ConvertMemberName(System.Reflection.MemberInfo member); + } + public partial class JsonObjectSerializer : Azure.Core.Serialization.ObjectSerializer, Azure.Core.Serialization.IMemberNameConverter { public JsonObjectSerializer() { } public JsonObjectSerializer(System.Text.Json.JsonSerializerOptions options) { } + string? Azure.Core.Serialization.IMemberNameConverter.ConvertMemberName(System.Reflection.MemberInfo member) { throw null; } public override object Deserialize(System.IO.Stream stream, System.Type returnType, System.Threading.CancellationToken cancellationToken) { throw null; } public override System.Threading.Tasks.ValueTask DeserializeAsync(System.IO.Stream stream, System.Type returnType, System.Threading.CancellationToken cancellationToken) { throw null; } public override void Serialize(System.IO.Stream stream, object? value, System.Type inputType, System.Threading.CancellationToken cancellationToken) { } diff --git a/sdk/core/Azure.Core/src/Serialization/IMemberNameConverter.cs b/sdk/core/Azure.Core/src/Serialization/IMemberNameConverter.cs new file mode 100644 index 000000000000..c900a25a1dd7 --- /dev/null +++ b/sdk/core/Azure.Core/src/Serialization/IMemberNameConverter.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Reflection; + +namespace Azure.Core.Serialization +{ + /// + /// Converts type member names to serializable member names. + /// + public interface IMemberNameConverter + { + /// + /// Converts a to a serializable member name. + /// + /// The to convert to a serializable member name. + /// The serializable member name, or null if the member is not defined or ignored by the serializer. + /// is null. + string? ConvertMemberName(MemberInfo member); + } +} diff --git a/sdk/core/Azure.Core/src/Serialization/JsonObjectSerializer.cs b/sdk/core/Azure.Core/src/Serialization/JsonObjectSerializer.cs index 9cd35c993e5e..5559deadeab6 100644 --- a/sdk/core/Azure.Core/src/Serialization/JsonObjectSerializer.cs +++ b/sdk/core/Azure.Core/src/Serialization/JsonObjectSerializer.cs @@ -2,8 +2,11 @@ // Licensed under the MIT License. using System; +using System.Collections.Concurrent; using System.IO; +using System.Reflection; using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; @@ -12,8 +15,9 @@ namespace Azure.Core.Serialization /// /// A implementation that uses to for serialization/deserialization. /// - public class JsonObjectSerializer : ObjectSerializer + public class JsonObjectSerializer : ObjectSerializer, IMemberNameConverter { + private readonly ConcurrentDictionary _cache; private readonly JsonSerializerOptions _options; /// @@ -27,9 +31,13 @@ public JsonObjectSerializer() : this(new JsonSerializerOptions()) /// Initializes new instance of . /// /// The instance to use when serializing/deserializing. + /// is null. public JsonObjectSerializer(JsonSerializerOptions options) { - _options = options; + _options = options ?? throw new ArgumentNullException(nameof(options)); + + // TODO: Consider using WeakReference cache to allow the GC to collect if the JsonObjectSerialized is held for a long duration. + _cache = new ConcurrentDictionary(); } /// @@ -58,5 +66,72 @@ public override async ValueTask DeserializeAsync(Stream stream, Type ret { return await JsonSerializer.DeserializeAsync(stream, returnType, _options, cancellationToken).ConfigureAwait(false); } + + /// + string? IMemberNameConverter.ConvertMemberName(MemberInfo member) + { + Argument.AssertNotNull(member, nameof(member)); + + return _cache.GetOrAdd(member, m => + { + // Mimics property enumeration based on: + // * https://github.com/dotnet/corefx/blob/v3.1.0/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs#L130-L191 + // * TODO: Add support for fields when .NET 5 GAs (https://github.com/Azure/azure-sdk-for-net/issues/13627) + + if (m is PropertyInfo propertyInfo) + { + // Ignore indexers. + if (propertyInfo.GetIndexParameters().Length > 0) + { + return null; + } + + // Only support public getters and/or setters. + if (propertyInfo.GetMethod?.IsPublic == true || + propertyInfo.SetMethod?.IsPublic == true) + { + if (propertyInfo.GetCustomAttribute() != null) + { + return null; + } + + // Ignore - but do not assert correctness - for JsonExtensionDataAttribute based on + // https://github.com/dotnet/corefx/blob/v3.1.0/src/System.Text.Json/src/System/Text/Json/Serialization/JsonClassInfo.cs#L244-L261 + if (propertyInfo.GetCustomAttribute() != null) + { + return null; + } + + // No need to validate collisions since they are based on the serialized name. + return GetPropertyName(propertyInfo); + } + } + + // The member is unsupported or ignored. + return null; + }); + } + + private string GetPropertyName(MemberInfo memberInfo) + { + // Mimics property name determination based on + // https://github.com/dotnet/runtime/blob/dc8b6f90/src/libraries/System.Text.Json/src/System/Text/Json/Serialization/JsonPropertyInfo.cs#L53-L90 + + JsonPropertyNameAttribute nameAttribute = memberInfo.GetCustomAttribute(false); + if (nameAttribute != null) + { + return nameAttribute.Name + ?? throw new InvalidOperationException($"The JSON property name for '{memberInfo.DeclaringType}.{memberInfo.Name}' cannot be null."); + } + else if (_options.PropertyNamingPolicy != null) + { + return _options.PropertyNamingPolicy.ConvertName(memberInfo.Name) + ?? throw new InvalidOperationException($"The JSON property name for '{memberInfo.DeclaringType}.{memberInfo.Name}' cannot be null."); + } + else + { + return memberInfo.Name; + } + } } -} \ No newline at end of file +} diff --git a/sdk/core/Azure.Core/src/Serialization/ObjectSerializer.cs b/sdk/core/Azure.Core/src/Serialization/ObjectSerializer.cs index fac8717095a1..28864fa0ddd2 100644 --- a/sdk/core/Azure.Core/src/Serialization/ObjectSerializer.cs +++ b/sdk/core/Azure.Core/src/Serialization/ObjectSerializer.cs @@ -9,7 +9,7 @@ namespace Azure.Core.Serialization { /// - /// An abstraction from reading typed objects. + /// An abstraction for reading typed objects. /// public abstract class ObjectSerializer { diff --git a/sdk/core/Azure.Core/tests/JsonObjectSerializerTest.cs b/sdk/core/Azure.Core/tests/JsonObjectSerializerTest.cs index e8201cb733eb..2bd46c6e47e8 100644 --- a/sdk/core/Azure.Core/tests/JsonObjectSerializerTest.cs +++ b/sdk/core/Azure.Core/tests/JsonObjectSerializerTest.cs @@ -1,71 +1,176 @@ // 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.Reflection; using System.Text; using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading.Tasks; using Azure.Core.Serialization; using NUnit.Framework; namespace Azure.Core.Tests { + [TestFixture(false)] + [TestFixture(true)] public class JsonObjectSerializerTest { - private static readonly JsonObjectSerializer JsonObjectSerializer = new JsonObjectSerializer(new JsonSerializerOptions() + private readonly JsonObjectSerializer _jsonObjectSerializer; + + public JsonObjectSerializerTest(bool camelCase) + { + _jsonObjectSerializer = new JsonObjectSerializer( + new JsonSerializerOptions + { + PropertyNamingPolicy = camelCase ? JsonNamingPolicy.CamelCase : null, + }); + + IsCamelCase = camelCase; + } + + public bool IsCamelCase { get; } + + private string SerializedName(string name) => IsCamelCase ? JsonNamingPolicy.CamelCase.ConvertName(name) : name; + + [Test] + public void ConstructorRequiresArgument() { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }); + ArgumentNullException ex = Assert.Throws(() => new JsonObjectSerializer(null)); + Assert.AreEqual("options", ex.ParamName); + } [Test] public void CanSerializeAnObject() { using var memoryStream = new MemoryStream(); - var o = new Model {A = "1", B = 2}; + var o = new ExtendedModel(readOnlyD: 4) + { + A = "1", + B = 2, + C = 3, + IgnoredE = 5, + F = 6, + }; - JsonObjectSerializer.Serialize(memoryStream, o, o.GetType(), default); + _jsonObjectSerializer.Serialize(memoryStream, o, o.GetType(), default); - Assert.AreEqual("{\"a\":\"1\",\"b\":2}", Encoding.UTF8.GetString(memoryStream.ToArray())); + Assert.AreEqual($"{{\"d\":4,\"{SerializedName("A")}\":\"1\",\"{SerializedName("B")}\":2}}", Encoding.UTF8.GetString(memoryStream.ToArray())); } [Test] public async Task CanSerializeAnObjectAsync() { using var memoryStream = new MemoryStream(); - var o = new Model {A = "1", B = 2}; + var o = new ExtendedModel(readOnlyD: 4) + { + A = "1", + B = 2, + C = 3, + IgnoredE = 5, + F = 6, + }; - await JsonObjectSerializer.SerializeAsync(memoryStream, o, o.GetType(), default); + await _jsonObjectSerializer.SerializeAsync(memoryStream, o, o.GetType(), default); - var aB = "{\"a\":\"1\",\"b\":2}"; - Assert.AreEqual(aB, Encoding.UTF8.GetString(memoryStream.ToArray())); + Assert.AreEqual($"{{\"d\":4,\"{SerializedName("A")}\":\"1\",\"{SerializedName("B")}\":2}}", Encoding.UTF8.GetString(memoryStream.ToArray())); } [Test] public void CanDeserializeAnObject() { - using var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes("{\"a\":\"1\",\"b\":2}")); + using var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes($"{{\"d\":4,\"{SerializedName("A")}\":\"1\",\"{SerializedName("B")}\":2}}")); - var model = (Model)JsonObjectSerializer.Deserialize(memoryStream, typeof(Model), default); + var model = (ExtendedModel)_jsonObjectSerializer.Deserialize(memoryStream, typeof(ExtendedModel), default); Assert.AreEqual("1", model.A); Assert.AreEqual(2, model.B); + Assert.AreEqual(0, model.C); + Assert.AreEqual(0, model.ReadOnlyD); + Assert.AreEqual(0, model.IgnoredE); + Assert.AreEqual(0, model.F); } [Test] public async Task CanDeserializeAnObjectAsync() { - using var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes("{\"a\":\"1\",\"b\":2}")); + using var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes($"{{\"d\":4,\"{SerializedName("A")}\":\"1\",\"{SerializedName("B")}\":2}}")); - var model = (Model)await JsonObjectSerializer.DeserializeAsync(memoryStream, typeof(Model), default).ConfigureAwait(false); + var model = (ExtendedModel)await _jsonObjectSerializer.DeserializeAsync(memoryStream, typeof(ExtendedModel), default).ConfigureAwait(false); Assert.AreEqual("1", model.A); Assert.AreEqual(2, model.B); + Assert.AreEqual(0, model.C); + Assert.AreEqual(0, model.ReadOnlyD); + Assert.AreEqual(0, model.IgnoredE); + Assert.AreEqual(0, model.F); + } + + [Test] + public void ConvertMemberName() + { + IEnumerable members = typeof(ExtendedModel) + .GetMembers(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(member => (member.MemberType & (MemberTypes.Property | MemberTypes.Field)) != 0); + + IMemberNameConverter converter = _jsonObjectSerializer; + + foreach (MemberInfo member in members) + { + string propertyName = converter.ConvertMemberName(member); + + // The following should be null for any property that does not serialized (compare to assertions above). + switch (member.Name) + { + case nameof(ExtendedModel.A): + Assert.AreEqual(SerializedName("A"), propertyName); + break; + + case nameof(ExtendedModel.B): + Assert.AreEqual(SerializedName("B"), propertyName); + break; + + case nameof(ExtendedModel.ReadOnlyD): + Assert.AreEqual("d", propertyName); + break; + + default: + Assert.IsNull(propertyName, $"Unexpected serialized name '{propertyName}' for member {member.DeclaringType}.{member.Name}"); + break; + } + } } public class Model { public string A { get; set; } + public int B { get; set; } + + [JsonIgnore] + public int C { get; set; } + } + + public class ExtendedModel : Model + { + public ExtendedModel() + { + } + + internal ExtendedModel(int readOnlyD) + { + ReadOnlyD = readOnlyD; + } + + [JsonPropertyName("d")] + public int ReadOnlyD { get; } + + internal int IgnoredE { get; set; } + + public int F; } } -} \ No newline at end of file +} diff --git a/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/CHANGELOG.md b/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/CHANGELOG.md new file mode 100644 index 000000000000..702468d387c6 --- /dev/null +++ b/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/CHANGELOG.md @@ -0,0 +1,7 @@ +# Release History + +## 1.0.0-preview.1 (Unreleased) + +### Added + +- Added `JsonObjectSerializer` that implements `ObjectSerializer` for `Newtonsoft.Json`, aka JSON.NET. diff --git a/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/Directory.Build.props b/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/Directory.Build.props new file mode 100644 index 000000000000..805ca8beaf23 --- /dev/null +++ b/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/Directory.Build.props @@ -0,0 +1,7 @@ + + + true + + + + diff --git a/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/README.md b/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/README.md new file mode 100644 index 000000000000..95de259fb693 --- /dev/null +++ b/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/README.md @@ -0,0 +1,49 @@ +# Newtonsoft.Json implementation for Azure Core Experimental shared client library for .NET + +Azure.Core.Experimental contains types that are being evaluated and might eventually become part of Azure.Core. +This library contains implementations dependent on Newtonsoft.Json, aka JSON.NET, for use with Azure.Core.Experimental. + +## Getting started + +TODO + +### Install the package + +TODO + +### Prerequisites + +TODO + +### Authenticate the client + +TODO + +## Key concepts + +TODO + +## Examples + +TODO + +## Troubleshooting + +TODO + +## Next steps + +TODO + +## Contributing + +This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For details, visit . + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct][code_of_conduct]. For more information see the [Code of Conduct FAQ][code_of_conduct_faq] or contact opencode@microsoft.com with any additional questions or comments. + +![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-net%2Fsdk%2Fcore%2FMicrosoft.Azure.Core.NewtonsoftJson%2FREADME.png) + +[code_of_conduct]: https://opensource.microsoft.com/codeofconduct +[code_of_conduct_faq]: https://opensource.microsoft.com/codeofconduct/faq/ diff --git a/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/api/Microsoft.Azure.Core.NewtonsoftJson.netstandard2.0.cs b/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/api/Microsoft.Azure.Core.NewtonsoftJson.netstandard2.0.cs new file mode 100644 index 000000000000..4e0715785f50 --- /dev/null +++ b/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/api/Microsoft.Azure.Core.NewtonsoftJson.netstandard2.0.cs @@ -0,0 +1,13 @@ +namespace Azure.Core.Serialization +{ + public partial class NewtonsoftJsonObjectSerializer : Azure.Core.Serialization.ObjectSerializer, Azure.Core.Serialization.IMemberNameConverter + { + public NewtonsoftJsonObjectSerializer() { } + public NewtonsoftJsonObjectSerializer(Newtonsoft.Json.JsonSerializerSettings settings) { } + string? Azure.Core.Serialization.IMemberNameConverter.ConvertMemberName(System.Reflection.MemberInfo member) { throw null; } + public override object Deserialize(System.IO.Stream stream, System.Type returnType, System.Threading.CancellationToken cancellationToken) { throw null; } + public override System.Threading.Tasks.ValueTask DeserializeAsync(System.IO.Stream stream, System.Type returnType, System.Threading.CancellationToken cancellationToken) { throw null; } + public override void Serialize(System.IO.Stream stream, object? value, System.Type inputType, System.Threading.CancellationToken cancellationToken) { } + public override System.Threading.Tasks.ValueTask SerializeAsync(System.IO.Stream stream, object? value, System.Type inputType, System.Threading.CancellationToken cancellationToken) { throw null; } + } +} diff --git a/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/src/Microsoft.Azure.Core.NewtonsoftJson.csproj b/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/src/Microsoft.Azure.Core.NewtonsoftJson.csproj new file mode 100644 index 000000000000..c73970232d52 --- /dev/null +++ b/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/src/Microsoft.Azure.Core.NewtonsoftJson.csproj @@ -0,0 +1,25 @@ + + + Implementation of Azure.Core serialization and type discovery for Newtonsoft.Json + Microsoft Azure Implementation for Newtonsoft.Json + Azure.Core + 1.0.0-preview.1 + Microsoft Azure Newtonsoft Json + enable + $(RequiredTargetFrameworks) + false + + + + + + + + + + + + + + + diff --git a/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/src/Serialization/NewtonsoftJsonObjectSerializer.cs b/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/src/Serialization/NewtonsoftJsonObjectSerializer.cs new file mode 100644 index 000000000000..11656de58d14 --- /dev/null +++ b/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/src/Serialization/NewtonsoftJsonObjectSerializer.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace Azure.Core.Serialization +{ + /// + /// A implementation that uses to for serialization/deserialization. + /// + public class NewtonsoftJsonObjectSerializer : ObjectSerializer, IMemberNameConverter + { + private const int DefaultBufferSize = 1024; + + // Older StreamReader and StreamWriter would otherwise default to this. + private static readonly Encoding UTF8NoBOM = new UTF8Encoding(false, true); + + private readonly ConcurrentDictionary _cache; + private readonly JsonSerializer _serializer; + + /// + /// Initializes new instance of . + /// + public NewtonsoftJsonObjectSerializer() : this(new JsonSerializerSettings()) + { + } + + /// + /// Initializes new instance of . + /// + /// The instance to use when serializing/deserializing. + /// is null. + public NewtonsoftJsonObjectSerializer(JsonSerializerSettings settings) + { + Argument.AssertNotNull(settings, nameof(settings)); + + _cache = new ConcurrentDictionary(); + _serializer = JsonSerializer.Create(settings); + } + + /// + /// or is null. + public override object Deserialize(Stream stream, Type returnType, CancellationToken cancellationToken) + { + Argument.AssertNotNull(stream, nameof(stream)); + Argument.AssertNotNull(returnType, nameof(returnType)); + + using StreamReader reader = new StreamReader(stream, UTF8NoBOM, true, DefaultBufferSize, true); + return _serializer.Deserialize(reader, returnType); + } + + /// + /// or is null. + public override ValueTask DeserializeAsync(Stream stream, Type returnType, CancellationToken cancellationToken) => + new ValueTask(Deserialize(stream, returnType, cancellationToken)); + + /// + /// or is null. + public override void Serialize(Stream stream, object? value, Type inputType, CancellationToken cancellationToken) + { + Argument.AssertNotNull(stream, nameof(stream)); + Argument.AssertNotNull(inputType, nameof(inputType)); + + using StreamWriter writer = new StreamWriter(stream, UTF8NoBOM, DefaultBufferSize, true); + _serializer.Serialize(writer, value, inputType); + } + + /// + /// or is null. + public override ValueTask SerializeAsync(Stream stream, object? value, Type inputType, CancellationToken cancellationToken) + { + Serialize(stream, value, inputType, cancellationToken); + return new ValueTask(); + } + + /// + string? IMemberNameConverter.ConvertMemberName(MemberInfo member) + { + Argument.AssertNotNull(member, nameof(member)); + + return _cache.GetOrAdd(member, m => + { + if (_serializer.ContractResolver.ResolveContract(m.ReflectedType) is JsonObjectContract contract) + { + foreach (JsonProperty property in contract.Properties) + { + if (!property.Ignored && + string.Equals(property.UnderlyingName, m.Name, StringComparison.Ordinal) && + property.DeclaringType == m.DeclaringType) + { + return property.PropertyName; + } + } + } + + return null; + }); + } + } +} diff --git a/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/tests/Microsoft.Azure.Core.NewtonsoftJson.Tests.csproj b/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/tests/Microsoft.Azure.Core.NewtonsoftJson.Tests.csproj new file mode 100644 index 000000000000..665b4a7247e5 --- /dev/null +++ b/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/tests/Microsoft.Azure.Core.NewtonsoftJson.Tests.csproj @@ -0,0 +1,17 @@ + + + Azure.Core.Tests + $(RequiredTargetFrameworks) + + + + + + + + + + + + + \ No newline at end of file diff --git a/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/tests/NewtonsoftJsonObjectSerializerTest.cs b/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/tests/NewtonsoftJsonObjectSerializerTest.cs new file mode 100644 index 000000000000..36a02b5c58be --- /dev/null +++ b/sdk/core/Microsoft.Azure.Core.NewtonsoftJson/tests/NewtonsoftJsonObjectSerializerTest.cs @@ -0,0 +1,269 @@ +// 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.Reflection; +using System.Text; +using System.Threading.Tasks; +using Azure.Core.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +using NUnit.Framework; + +namespace Azure.Core.Tests +{ + [TestFixture(false)] + [TestFixture(true)] + public class NewtonsoftJsonObjectSerializerTest + { + private readonly NewtonsoftJsonObjectSerializer _jsonObjectSerializer; + private readonly DefaultContractResolver _resolver; + + public NewtonsoftJsonObjectSerializerTest(bool camelCase) + { + // Use contract resolvers that sort the serialized properties case-insensitively for deterministic assertions. + _resolver = camelCase ? (DefaultContractResolver)new SortedCamelCasePropertyNamesContractResolver() : new SortedDefaultContractResolver(); + + _jsonObjectSerializer = new NewtonsoftJsonObjectSerializer(new JsonSerializerSettings + { + ContractResolver = _resolver, + Converters = new[] + { + new StringEnumConverter(true), + }, + }); + + IsCamelCase = camelCase; + } + + public bool IsCamelCase { get; } + + private string SerializedName(string name) => _resolver.GetResolvedPropertyName(name); + + + [Test] + public void ConstructorRequiresArgument() + { + ArgumentNullException ex = Assert.Throws(() => new NewtonsoftJsonObjectSerializer(null)); + Assert.AreEqual("settings", ex.ParamName); + } + + [Test] + public void CanSerializeAnObject() + { + using var memoryStream = new MemoryStream(); + var o = new ExtendedModel(4, 8) + { + A = "1", + ActuallyB = 2, + C = 3, + Type = ModelType.One, + IgnoredE = 5, + F = 6, + G = 7, + }; + + _jsonObjectSerializer.Serialize(memoryStream, o, o.GetType(), default); + + Assert.AreEqual($"{{\"{SerializedName("A")}\":\"1\",\"b\":2,\"d\":4,\"{SerializedName("F")}\":6,\"h\":8,\"{SerializedName("Type")}\":\"one\"}}", Encoding.UTF8.GetString(memoryStream.ToArray())); + } + + [Test] + public async Task CanSerializeAnObjectAsync() + { + using var memoryStream = new MemoryStream(); + var o = new ExtendedModel(4, 8) + { + A = "1", + ActuallyB = 2, + C = 3, + Type = ModelType.One, + IgnoredE = 5, + F = 6, + G = 7, + }; + + await _jsonObjectSerializer.SerializeAsync(memoryStream, o, o.GetType(), default); + + Assert.AreEqual($"{{\"{SerializedName("A")}\":\"1\",\"b\":2,\"d\":4,\"{SerializedName("F")}\":6,\"h\":8,\"{SerializedName("Type")}\":\"one\"}}", Encoding.UTF8.GetString(memoryStream.ToArray())); + } + + [Test] + public void CanDeserializeAnObject() + { + var json = $@"{{ + ""{SerializedName("A")}"": ""1"", + ""b"": 2, + ""{SerializedName("C")}"": 3, + ""{SerializedName("Type")}"": ""one"", + ""d"": 4, + ""{SerializedName("IgnoredE")}"": 5, + ""{SerializedName("F")}"": 6, + ""{SerializedName("G")}"": 7, + ""h"": 8 +}}"; + using var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + + var model = (ExtendedModel)_jsonObjectSerializer.Deserialize(memoryStream, typeof(ExtendedModel), default); + + Assert.AreEqual("1", model.A); + Assert.AreEqual(2, model.ActuallyB); + Assert.AreEqual(0, model.C); + Assert.AreEqual(ModelType.One, model.Type); + Assert.AreEqual(0, model.ReadOnlyD); + Assert.AreEqual(0, model.IgnoredE); + Assert.AreEqual(6, model.F); + Assert.AreEqual(0, model.G); + Assert.AreEqual(8, model.GetH()); + } + + [Test] + public async Task CanDeserializeAnObjectAsync() + { + var json = $@"{{ + ""{SerializedName("A")}"": ""1"", + ""b"": 2, + ""{SerializedName("C")}"": 3, + ""{SerializedName("Type")}"": ""one"", + ""d"": 4, + ""{SerializedName("IgnoredE")}"": 5, + ""{SerializedName("F")}"": 6, + ""{SerializedName("G")}"": 7, + ""h"": 8 +}}"; + using var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(json)); + + var model = (ExtendedModel)await _jsonObjectSerializer.DeserializeAsync(memoryStream, typeof(ExtendedModel), default).ConfigureAwait(false); + + Assert.AreEqual("1", model.A); + Assert.AreEqual(2, model.ActuallyB); + Assert.AreEqual(0, model.C); + Assert.AreEqual(ModelType.One, model.Type); + Assert.AreEqual(0, model.ReadOnlyD); + Assert.AreEqual(0, model.IgnoredE); + Assert.AreEqual(6, model.F); + Assert.AreEqual(0, model.G); + Assert.AreEqual(8, model.GetH()); + } + + [Test] + public void ConvertMemberName() + { + IEnumerable members = typeof(ExtendedModel) + .GetMembers(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) + .Where(member => (member.MemberType & (MemberTypes.Property | MemberTypes.Field)) != 0); + + IMemberNameConverter converter = _jsonObjectSerializer; + + foreach (MemberInfo member in members) + { + string propertyName = converter.ConvertMemberName(member); + + // The following should be null for any property that does not serialized (compare to assertions above). + switch (member.Name) + { + case nameof(ExtendedModel.A): + Assert.AreEqual(SerializedName("A"), propertyName); + break; + + case nameof(ExtendedModel.ActuallyB): + Assert.AreEqual("b", propertyName); + break; + + case nameof(ExtendedModel.Type): + Assert.AreEqual(SerializedName("Type"), propertyName); + break; + + case nameof(ExtendedModel.ReadOnlyD): + Assert.AreEqual("d", propertyName); + break; + + case nameof(ExtendedModel.F): + Assert.AreEqual(SerializedName("F"), propertyName); + break; + + case "H": + Assert.AreEqual("h", propertyName); + break; + + default: + Assert.IsNull(propertyName, $"Unexpected serialized name '{propertyName}' for member {member.DeclaringType}.{member.Name}"); + break; + } + } + } + + public class Model + { + public string A { get; set; } + + [JsonProperty("b")] + public int ActuallyB { get; set; } + + [JsonIgnore] + public int C { get; set; } + + public ModelType Type { get; set; } + } + + public class ExtendedModel : Model + { + public ExtendedModel() + { + } + + internal ExtendedModel(int readOnlyD, int h) + { + ReadOnlyD = readOnlyD; + H = h; + } + + [JsonProperty("d")] + public int ReadOnlyD { get; } + + internal int IgnoredE { get; set; } + + public int F; + +#pragma warning disable CS0649 // Field 'NewtonsoftJsonObjectSerializerTest.ExtendedModel.G' is never assigned to, and will always have its default value 0 + internal int G; +#pragma warning restore CS0649 + +#pragma warning disable CS0169 // The field 'NewtonsoftJsonObjectSerializerTest.ExtendedModel.H' is never used + [JsonProperty("h")] + private int H; +#pragma warning restore CS0169 + + internal int GetH() => H; + } + + public enum ModelType + { + Unknown = 0, + One = 1, + Two = 2, + } + + private class SortedCamelCasePropertyNamesContractResolver : CamelCasePropertyNamesContractResolver + { + // Make sure all properties and fields are sorted case-insensitively for deterministic assertions. + protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) => + base.CreateProperties(type, memberSerialization) + .OrderBy(property => property.PropertyName, StringComparer.OrdinalIgnoreCase) + .ToList(); + } + + private class SortedDefaultContractResolver : DefaultContractResolver + { + // Make sure all properties and fields are sorted case-insensitively for deterministic assertions. + protected override IList CreateProperties(Type type, MemberSerialization memberSerialization) => + base.CreateProperties(type, memberSerialization) + .OrderBy(property => property.PropertyName, StringComparer.OrdinalIgnoreCase) + .ToList(); + } + } +} \ No newline at end of file diff --git a/sdk/core/ci.yml b/sdk/core/ci.yml index 80274796f494..9204abd5b8c9 100644 --- a/sdk/core/ci.yml +++ b/sdk/core/ci.yml @@ -41,4 +41,6 @@ extends: - name: Azure.Core.Experimental safeName: AzureCoreExperimental - name: Microsoft.Extensions.Azure - safeName: MicrosoftExtensionsAzure \ No newline at end of file + safeName: MicrosoftExtensionsAzure + - name: Microsoft.Azure.Core.NewtonsoftJson + safeName: MicrosoftAzureCoreNewtonsoftJson