From f09fa378cbd3a7b277bcdade345f37c485e60466 Mon Sep 17 00:00:00 2001 From: Bill Menees Date: Sun, 9 Oct 2022 08:43:58 -0500 Subject: [PATCH] Add ReadOnlyDictionary converter --- src/Menees.Remoting/Json/JSerializer.cs | 3 + .../Json/ReadOnlyDictionaryConverter.cs | 111 ++++++++++++++++++ .../NodeSettingsTests.cs | 23 +++- 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 src/Menees.Remoting/Json/ReadOnlyDictionaryConverter.cs diff --git a/src/Menees.Remoting/Json/JSerializer.cs b/src/Menees.Remoting/Json/JSerializer.cs index fd2fbf0..1da247a 100644 --- a/src/Menees.Remoting/Json/JSerializer.cs +++ b/src/Menees.Remoting/Json/JSerializer.cs @@ -26,6 +26,9 @@ internal sealed class JSerializer : ISerializer // Handle user vs. system serialized values. new UserSerializedValueConverter(), + + // Handle System.Collections.ObjectModel.ReadOnlyDictionary. + new ReadOnlyDictionaryConverter(), }, // Make sure ValueTuple serializes correctly since it uses public fields. diff --git a/src/Menees.Remoting/Json/ReadOnlyDictionaryConverter.cs b/src/Menees.Remoting/Json/ReadOnlyDictionaryConverter.cs new file mode 100644 index 0000000..8d63058 --- /dev/null +++ b/src/Menees.Remoting/Json/ReadOnlyDictionaryConverter.cs @@ -0,0 +1,111 @@ +namespace Menees.Remoting.Json; + +#region Using Directives + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +#endregion + +/// +/// Used to convert System.Collections.ObjectModel.ReadOnlyDictionary<TKey,TValue>. +/// +/// +/// Microsoft's built-in converters handle converting IReadOnlyDictionary as a writable Dictionary +/// instance. This converter is required to deserialize a concrete ReadOnlyDictionary instance +/// since it doesn't have a default constructor. This deserializes the data into a writable +/// Dictionary instance then passes that to the ReadOnlyDictionary constructor. +/// +/// Based on https://stackoverflow.com/a/70813056/1882616, which was +/// based on https://gist.github.com/mikaeldui/1383dda4147f461ac4154406c03cc180. +/// +internal sealed class ReadOnlyDictionaryConverter : JsonConverterFactory +{ + #region Public Methods + + public override bool CanConvert(Type typeToConvert) + { + // The typeToConvert must be ReadOnlyDictionary<,> or derived from it, and it must expose IReadOnlyDictionary<,>. + bool result = typeToConvert.IsGenericType + && typeToConvert.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)) + && (typeToConvert.GetGenericTypeDefinition() == typeof(ReadOnlyDictionary<,>) + || IsSubclassOfOpenGeneric(typeof(ReadOnlyDictionary<,>), typeToConvert)); + return result; + } + + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + Type iReadOnlyDictionary = typeToConvert.GetInterfaces().First(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)); + Type keyType = iReadOnlyDictionary.GetGenericArguments()[0]; + Type valueType = iReadOnlyDictionary.GetGenericArguments()[1]; + + JsonConverter? converter = (JsonConverter?)Activator.CreateInstance( + typeof(ReadOnlyDictionaryConverterInner<,>).MakeGenericType(keyType, valueType), + BindingFlags.Instance | BindingFlags.Public, + binder: null, + args: null, + culture: null); + + return converter; + } + + #endregion + + #region Private Methods + + // Based on https://stackoverflow.com/a/457708/1882616. + private static bool IsSubclassOfOpenGeneric(Type generic, Type? toCheck) + { + bool result = false; + + while (toCheck != null && toCheck != typeof(object)) + { + Type current = toCheck.IsGenericType ? toCheck.GetGenericTypeDefinition() : toCheck; + if (generic == current) + { + result = true; + break; + } + + toCheck = toCheck.BaseType; + } + + return result; + } + + #endregion + + #region Private Types + + private class ReadOnlyDictionaryConverterInner : JsonConverter> + where TKey : notnull + { + public override IReadOnlyDictionary? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + Dictionary? dictionary = JsonSerializer.Deserialize>(ref reader, options: options); + + IReadOnlyDictionary? result = null; + if (dictionary != null) + { + result = (IReadOnlyDictionary?)Activator.CreateInstance( + typeToConvert, + BindingFlags.Instance | BindingFlags.Public, + binder: null, + args: new object[] { dictionary }, + culture: null); + } + + return result; + } + + public override void Write(Utf8JsonWriter writer, IReadOnlyDictionary dictionary, JsonSerializerOptions options) => + JsonSerializer.Serialize(writer, dictionary, options); + } + + #endregion +} diff --git a/tests/Menees.Remoting.Tests/NodeSettingsTests.cs b/tests/Menees.Remoting.Tests/NodeSettingsTests.cs index 271871a..840c25f 100644 --- a/tests/Menees.Remoting.Tests/NodeSettingsTests.cs +++ b/tests/Menees.Remoting.Tests/NodeSettingsTests.cs @@ -1,7 +1,9 @@ namespace Menees.Remoting; +using System.Collections.ObjectModel; + [TestClass] -public class NodeSettingsTests +public class NodeSettingsTests : BaseTests { [TestMethod] public void RequireGetType() @@ -51,4 +53,23 @@ static string AdjustVersion(string typeName, uint majorVersion) return result; } } + + [TestMethod] + public async Task ConvertReadOnlyDictionaryAsync() + { + string serverPath = this.GenerateServerPath(); + + using MessageServer, ReadOnlyDictionary> server = new( + async dictionary => await Task.FromResult(dictionary).ConfigureAwait(false), + serverPath, + maxListeners: 10, + loggerFactory: this.LoggerFactory); + server.Start(); + + using MessageClient, ReadOnlyDictionary> client = new(serverPath, loggerFactory: this.LoggerFactory); + Dictionary plainDictionary = new() { [1] = "A", [2] = "B" }; + ReadOnlyDictionary readOnlyDictionary = new(plainDictionary); + ReadOnlyDictionary result = await client.SendAsync(readOnlyDictionary).ConfigureAwait(false); + result.ShouldBe(readOnlyDictionary); + } } \ No newline at end of file