Skip to content

Commit

Permalink
Add ReadOnlyDictionary converter
Browse files Browse the repository at this point in the history
  • Loading branch information
menees committed Oct 9, 2022
1 parent 570fe99 commit f09fa37
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 1 deletion.
3 changes: 3 additions & 0 deletions src/Menees.Remoting/Json/JSerializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ internal sealed class JSerializer : ISerializer

// Handle user vs. system serialized values.
new UserSerializedValueConverter(),

// Handle System.Collections.ObjectModel.ReadOnlyDictionary<TKey,TValue>.
new ReadOnlyDictionaryConverter(),
},

// Make sure ValueTuple serializes correctly since it uses public fields.
Expand Down
111 changes: 111 additions & 0 deletions src/Menees.Remoting/Json/ReadOnlyDictionaryConverter.cs
Original file line number Diff line number Diff line change
@@ -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

/// <summary>
/// Used to convert System.Collections.ObjectModel.ReadOnlyDictionary&lt;TKey,TValue>.
/// </summary>
/// <remarks>
/// 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.
/// <para/>
/// Based on https://stackoverflow.com/a/70813056/1882616, which was
/// based on https://gist.github.com/mikaeldui/1383dda4147f461ac4154406c03cc180.
/// </remarks>
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<TKey, TValue> : JsonConverter<IReadOnlyDictionary<TKey, TValue>>
where TKey : notnull
{
public override IReadOnlyDictionary<TKey, TValue>? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
Dictionary<TKey, TValue>? dictionary = JsonSerializer.Deserialize<Dictionary<TKey, TValue>>(ref reader, options: options);

IReadOnlyDictionary<TKey, TValue>? result = null;
if (dictionary != null)
{
result = (IReadOnlyDictionary<TKey, TValue>?)Activator.CreateInstance(
typeToConvert,
BindingFlags.Instance | BindingFlags.Public,
binder: null,
args: new object[] { dictionary },
culture: null);
}

return result;
}

public override void Write(Utf8JsonWriter writer, IReadOnlyDictionary<TKey, TValue> dictionary, JsonSerializerOptions options) =>
JsonSerializer.Serialize(writer, dictionary, options);
}

#endregion
}
23 changes: 22 additions & 1 deletion tests/Menees.Remoting.Tests/NodeSettingsTests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
namespace Menees.Remoting;

using System.Collections.ObjectModel;

[TestClass]
public class NodeSettingsTests
public class NodeSettingsTests : BaseTests
{
[TestMethod]
public void RequireGetType()
Expand Down Expand Up @@ -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<int, string>, ReadOnlyDictionary<int, string>> server = new(
async dictionary => await Task.FromResult(dictionary).ConfigureAwait(false),
serverPath,
maxListeners: 10,
loggerFactory: this.LoggerFactory);
server.Start();

using MessageClient<ReadOnlyDictionary<int, string>, ReadOnlyDictionary<int, string>> client = new(serverPath, loggerFactory: this.LoggerFactory);
Dictionary<int, string> plainDictionary = new() { [1] = "A", [2] = "B" };
ReadOnlyDictionary<int, string> readOnlyDictionary = new(plainDictionary);
ReadOnlyDictionary<int, string> result = await client.SendAsync(readOnlyDictionary).ConfigureAwait(false);
result.ShouldBe(readOnlyDictionary);
}
}

0 comments on commit f09fa37

Please sign in to comment.