CSharp · Console
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.Serialization.Formatters;
using System.IO;
using System.Data;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Reflection;
using System.Collections.ObjectModel;
using System.Collections.Concurrent;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.Json.Serialization.Metadata;
using System.Text.Json.Nodes;
using NUnit.Framework; // Throws NUnit.Framework.AssertionException
//https://stackoverflow.com/questions/78216715/serializing-custom-types-in-a-c-sharp-concurrentdictionary
#nullable enable
public class DictionaryValueConverterDecorator<TItemConverter> : JsonConverterFactory where TItemConverter : JsonConverter, new()
{
readonly TItemConverter itemConverter = new TItemConverter();
public override bool CanConvert(Type typeToConvert) =>
GetStringKeyedDictionaryValueType(typeToConvert) != null && GetConcreteTypeToConvert(typeToConvert) != null;
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
if (!(GetStringKeyedDictionaryValueType(typeToConvert) is {} valueType)
|| !(GetConcreteTypeToConvert(typeToConvert) is {} concreteTypeToConvert))
throw new ArgumentException($"Invalid type {typeToConvert}");
// Clone the incoming options and insert the item converter at the beginning of the clone.
// Then if converter is actually a JsonConverterFactory (e.g. JsonStringEnumConverter) then the correct JsonConverter<T> will be manufactured or fetched.
var modifiedOptions = new JsonSerializerOptions(options);
modifiedOptions.Converters.Insert(0, itemConverter);
var actualInnerConverter = modifiedOptions.GetConverter(valueType);
return (JsonConverter)Activator.CreateInstance(typeof(DictionaryValueConverterDecoratorInner<,>).MakeGenericType(new [] { concreteTypeToConvert, valueType }), new object [] { actualInnerConverter })!;
}
static Type? GetStringKeyedDictionaryValueType(Type typeToConvert)
{
if (!(typeToConvert.GetDictionaryKeyValueType() is {} types))
return null;
if (types[0] != typeof(string))
return null;
return types[1];
}
static Type? GetConcreteTypeToConvert(Type typeToConvert) =>
typeToConvert.IsInterface switch
{
false when typeToConvert.GetConstructor(Type.EmptyTypes) != null =>
typeToConvert,
true when typeToConvert.IsGenericType && typeToConvert.GetGenericTypeDefinition() is var generic && (generic == typeof(IDictionary<,>) || generic == typeof(IReadOnlyDictionary<,>)) =>
typeof(Dictionary<,>).MakeGenericType(typeToConvert.GetGenericArguments()),
_ => null,
};
}
internal class DictionaryValueConverterDecoratorInner<TDictionary, TValue> : JsonConverter<TDictionary> where TDictionary : IDictionary<string, TValue>, new()
{
readonly JsonConverter<TValue> innerConverter;
public DictionaryValueConverterDecoratorInner(JsonConverter<TValue> innerConverter) => this.innerConverter = innerConverter ?? throw new ArgumentNullException(nameof(innerConverter));
public override TDictionary? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.Null)
return (TDictionary?)(object?)null;
else if (reader.TokenType != JsonTokenType.StartObject)
throw new JsonException();
var dictionary = new TDictionary();
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
break;
else if (reader.TokenType != JsonTokenType.PropertyName)
throw new JsonException();
var key = reader.GetString().ThrowOnNull();
reader.ReadAndAssert();
var value = innerConverter.Read(ref reader, typeof(TValue), options);
dictionary.Add(key, value!);
}
return dictionary;
}
public override void Write(Utf8JsonWriter writer, TDictionary value, JsonSerializerOptions options)
{
writer.WriteStartObject();
foreach (var pair in value)
{
writer.WritePropertyName(pair.Key);
if (value == null && !innerConverter.HandleNull)
writer.WriteNullValue();
else
innerConverter.Write(writer, pair.Value, options);
}
writer.WriteEndObject();
}
}
public static partial class JsonExtensions
{
public static ref Utf8JsonReader ReadAndAssert(ref this Utf8JsonReader reader) { if (!reader.Read()) { throw new JsonException(); } return ref reader; }
public static T ThrowOnNull<T>(this T? value, [System.Runtime.CompilerServices.CallerArgumentExpression(nameof(value))] string? paramName = null) where T : class => value ?? throw new ArgumentNullException(paramName);
}
public static class TypeExtensions
{
public static IEnumerable<Type> GetInterfacesAndSelf(this Type type)
=> (type ?? throw new ArgumentNullException()).IsInterface ? new[] { type }.Concat(type.GetInterfaces()) : type.GetInterfaces();
public static IEnumerable<Type []> GetDictionaryKeyValueTypes(this Type type)
=> type.GetInterfacesAndSelf().Where(t => t.IsGenericType && t.GetGenericTypeDefinition() == typeof(IDictionary<,>)).Select(t => t.GetGenericArguments());
public static Type []? GetDictionaryKeyValueType(this Type type)
=> type.GetDictionaryKeyValueTypes().SingleOrDefaultIfMultiple();
}
public static class LinqExtensions
{
// Copied from this answer https://stackoverflow.com/a/25319572
// By https://stackoverflow.com/users/3542863/sean-rose
// To https://stackoverflow.com/questions/3185067/singleordefault-throws-an-exception-on-more-than-one-element
public static TSource? SingleOrDefaultIfMultiple<TSource>(this IEnumerable<TSource> source)
{
var elements = source.Take(2).ToArray();
return (elements.Length == 1) ? elements[0] : default(TSource);
}
}
//=======================================
public partial class Model
{
//[JsonProperty(PropertyName = "substances", ItemConverterType = typeof(SaturationJsonConverter))]
[JsonPropertyName("substances"),
JsonInclude,
JsonConverter(typeof(DictionaryValueConverterDecorator<SaturationJsonConverter>))]
protected ConcurrentDictionary<string, Saturation> Substances { get; private set; } = new();
}
public partial class Model
{
[JsonIgnore]
public IDictionary<string, Saturation> SubstancesSurrogate => Substances;
}
public class Saturation
{
public string? Value { get; set; }
}
public class SaturationJsonConverter : JsonConverter<Saturation>
{
public override Saturation? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
new Saturation { Value = reader.GetString() };
public override void Write(Utf8JsonWriter writer, Saturation? value, JsonSerializerOptions options) =>
// Write the value as a string
writer.WriteStringValue(value?.Value);
}
class TestClass
{
public static void Test()
{
var model = new Model
{
SubstancesSurrogate =
{
["Item1"] = new Saturation { Value = "Item1" },
["Item2"] = new Saturation { Value = "Item2" },
},
};
var json = JsonSerializer.Serialize(model);
Console.WriteLine(json);
Assert.AreEqual("""{"substances":{"Item2":"Item2","Item1":"Item1"}}""", json);
var model2 = JsonSerializer.Deserialize<Model>(json);
var json2 = JsonSerializer.Serialize(model2);
Assert.That(JsonNode.DeepEquals(JsonNode.Parse(json), JsonNode.Parse(json2)));
Assert.That(model.SubstancesSurrogate.Count == model2?.SubstancesSurrogate.Count);
}
}
public class Program
{
public static void Main()
{
Console.WriteLine("Environment version: {0} ({1}), {2}", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , Environment.Version, Environment.OSVersion);
Console.WriteLine("System.Text.Json version: " + typeof(JsonSerializer).Assembly.FullName);
Console.WriteLine();
try
{
TestClass.Test();
}
catch (Exception ex)
{
Console.WriteLine("Failed with unhandled exception: ");
Console.WriteLine(ex);
throw;
}
}
}