From d1f08741d0187c765366881994d28f1bf8e00824 Mon Sep 17 00:00:00 2001 From: "o.nadymov" Date: Mon, 17 Jun 2024 15:50:06 +0300 Subject: [PATCH] JsonTypeConverter --- src/Spoleto.Common.Tests/JsonHelperTests.cs | 25 +++++ .../Objects/TestTypeClass.cs | 9 ++ src/Spoleto.Common/Helpers/JsonHelper.cs | 6 +- .../Helpers/SerializedTypeHelper.cs | 100 ++++++++++++++++++ .../JsonConverters/JsonTypeConverter.cs | 53 ++++++++++ src/Spoleto.Common/Objects/WebType.cs | 68 ++++++++++++ 6 files changed, 257 insertions(+), 4 deletions(-) create mode 100644 src/Spoleto.Common.Tests/Objects/TestTypeClass.cs create mode 100644 src/Spoleto.Common/Helpers/SerializedTypeHelper.cs create mode 100644 src/Spoleto.Common/JsonConverters/JsonTypeConverter.cs create mode 100644 src/Spoleto.Common/Objects/WebType.cs diff --git a/src/Spoleto.Common.Tests/JsonHelperTests.cs b/src/Spoleto.Common.Tests/JsonHelperTests.cs index ae74a5e..183dab4 100644 --- a/src/Spoleto.Common.Tests/JsonHelperTests.cs +++ b/src/Spoleto.Common.Tests/JsonHelperTests.cs @@ -1,4 +1,5 @@ using Spoleto.Common.Helpers; +using Spoleto.Common.Tests.Objects; namespace Spoleto.Common.Tests { @@ -86,5 +87,29 @@ public void EnumWithIntEnum() Assert.That(json.Contains("\"200\""), Is.False); }); } + [Test] + public void EnumWithType() + { + // Arrange + var systemType = typeof(int); + var myType = typeof(TestClass); + var obj = new TestTypeClass + { + MyType = myType, + SystemType = systemType + }; + + // Act + var json = JsonHelper.ToJson(obj); + var fromJson = JsonHelper.FromJson(json); + + // Assert + Assert.Multiple(() => + { + + Assert.That(fromJson.MyType, Is.EqualTo(obj.MyType)); + Assert.That(fromJson.SystemType, Is.EqualTo(obj.SystemType)); + }); + } } } diff --git a/src/Spoleto.Common.Tests/Objects/TestTypeClass.cs b/src/Spoleto.Common.Tests/Objects/TestTypeClass.cs new file mode 100644 index 0000000..d76cd31 --- /dev/null +++ b/src/Spoleto.Common.Tests/Objects/TestTypeClass.cs @@ -0,0 +1,9 @@ +namespace Spoleto.Common.Tests.Objects +{ + public class TestTypeClass + { + public Type MyType { get; set; } + + public Type SystemType { get; set; } + } +} diff --git a/src/Spoleto.Common/Helpers/JsonHelper.cs b/src/Spoleto.Common/Helpers/JsonHelper.cs index 535ac49..161da3d 100644 --- a/src/Spoleto.Common/Helpers/JsonHelper.cs +++ b/src/Spoleto.Common/Helpers/JsonHelper.cs @@ -1,14 +1,11 @@ using System; -using System.Collections; -using System.Collections.Generic; using System.IO; -using System.Linq; using System.Text.Encodings.Web; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Unicode; using System.Threading.Tasks; -using System.Web; +using Spoleto.Common.JsonConverters; namespace Spoleto.Common.Helpers { @@ -29,6 +26,7 @@ static JsonHelper() }; _defaultSerializerOptions.Converters.Add(new JsonStringEnumConverter()); + _defaultSerializerOptions.Converters.Add(new JsonTypeConverter()); } /// diff --git a/src/Spoleto.Common/Helpers/SerializedTypeHelper.cs b/src/Spoleto.Common/Helpers/SerializedTypeHelper.cs new file mode 100644 index 0000000..3cb49de --- /dev/null +++ b/src/Spoleto.Common/Helpers/SerializedTypeHelper.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace Spoleto.Common.Helpers +{ + /// + /// The helper for serialization of types. + /// + public static class SerializedTypeHelper + { + /// + /// Custom binder type. + /// + public static Func BindToType { get; set; } = DefaultBindToType; + + /// + /// Gets the type name does not have Version, Culture, Public token. + /// + public static bool RemoveAssemblyVersion = true; + + private static readonly Regex SubtractFullNameRegex = new Regex(@", Version=\d+.\d+.\d+.\d+, Culture=\w+, PublicKeyToken=\w+", RegexOptions.Compiled); + + // mscorlib or System.Private.CoreLib + private static readonly bool IsMscorlib = typeof(int).AssemblyQualifiedName.Contains("mscorlib"); + + private static readonly Dictionary TypeCache = []; + + /// + /// Deserializes the type. + /// + /// + /// + public static Type DeserializeType(string typeName) + { + lock (((ICollection)TypeCache).SyncRoot) + { + if (!TypeCache.TryGetValue(typeName, out var type)) + { + type = BindToType(typeName); + if (type == null) + { + if (IsMscorlib && typeName.Contains("System.Private.CoreLib")) + { + typeName = typeName.Replace("System.Private.CoreLib", "mscorlib"); + type = Type.GetType(typeName); + } + else if (!IsMscorlib && typeName.Contains("mscorlib")) + { + typeName = typeName.Replace("mscorlib", "System.Private.CoreLib"); + type = Type.GetType(typeName); + } + else + { + type = Type.GetType(typeName); + } + } + + if (type == null) + { + throw new TypeLoadException($"Cannot load type by the name <{typeName}>."); + } + + TypeCache[typeName] = type; + } + + return type; + } + } + + /// + /// Builds the type name for serialization. + /// + /// + /// + public static string BuildTypeName(Type type) + { + if (RemoveAssemblyVersion) + { + string full = type.AssemblyQualifiedName; + + var shortened = SubtractFullNameRegex.Replace(full, String.Empty); + if (Type.GetType(shortened) == null) + { + // if type cannot be found with shortened name - use full name + shortened = full; + } + + return shortened; + } + else + { + return type.AssemblyQualifiedName; + } + } + + private static Type DefaultBindToType(string typeName) => Type.GetType(typeName); + } +} diff --git a/src/Spoleto.Common/JsonConverters/JsonTypeConverter.cs b/src/Spoleto.Common/JsonConverters/JsonTypeConverter.cs new file mode 100644 index 0000000..2f7b6a2 --- /dev/null +++ b/src/Spoleto.Common/JsonConverters/JsonTypeConverter.cs @@ -0,0 +1,53 @@ +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using Spoleto.Common.Helpers; + +namespace Spoleto.Common.JsonConverters +{ + /// + /// The custom Json converter for the . + /// + public class JsonTypeConverter : JsonConverter + { + /// + /// The default constructor to initialize a Json converter for the . + /// + public JsonTypeConverter() + { + } + + public override Type Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return null; + + var typeName = reader.GetString(); + + if (typeName == null) + return null; + + var resolvedType = SerializedTypeHelper.DeserializeType(typeName); + + if (resolvedType == null) + { + throw new JsonException($"Unable to resolve type {typeName}"); + } + + return resolvedType; + } + + public override void Write(Utf8JsonWriter writer, Type value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + return; + } + + // Write the assembly-qualified name + var typeName = SerializedTypeHelper.BuildTypeName(value); + writer.WriteStringValue(typeName); + } + } +} diff --git a/src/Spoleto.Common/Objects/WebType.cs b/src/Spoleto.Common/Objects/WebType.cs new file mode 100644 index 0000000..dc866ae --- /dev/null +++ b/src/Spoleto.Common/Objects/WebType.cs @@ -0,0 +1,68 @@ +using System; +using System.Runtime.Serialization; +using Spoleto.Common.Helpers; + +namespace Spoleto.Common.Objects +{ + /// + /// String representation of Type. + /// + [DataContract] + [Serializable] + public class WebType + { + /// + /// Default constructor. + /// + public WebType() + { + } + + /// + /// Constructor with parameter. + /// + /// + public WebType(Type type) + { + if (type != null) + { + TypeName = SerializedTypeHelper.BuildTypeName(type); + } + } + + /// + /// Full type name with assembly name. + /// + [DataMember] + public string TypeName { get; set; } + + /// + /// Gets the real type. + /// + public Type GetRealType() => TypeName != null ? SerializedTypeHelper.DeserializeType(TypeName) : null; + + /// + /// User-defined conversion from Type to WebType. + /// + /// + public static explicit operator WebType(Type type) + { + return new WebType(type); + } + + /// + /// User-defined conversion from WebType to Type. + /// + /// + public static explicit operator Type(WebType webType) + { + return webType?.GetRealType(); + } + + /// + /// Returns the string representation. + /// + /// + public override String ToString() => TypeName; + } +}