diff --git a/src/core/Akka.Tests/Serialization/NewtonSoftJsonSerializerSetupSpec.cs b/src/core/Akka.Tests/Serialization/NewtonSoftJsonSerializerSetupSpec.cs index e798cbefdfd..43edb034ae6 100644 --- a/src/core/Akka.Tests/Serialization/NewtonSoftJsonSerializerSetupSpec.cs +++ b/src/core/Akka.Tests/Serialization/NewtonSoftJsonSerializerSetupSpec.cs @@ -45,14 +45,14 @@ internal class DummyContractResolver : DefaultContractResolver public static readonly ActorSystemSetup ActorSystemSettings = ActorSystemSetup.Create(SerializationSettings, Bootstrap); - public NewtonSoftJsonSerializerSetupSpec(ITestOutputHelper output) + public NewtonSoftJsonSerializerSetupSpec(ITestOutputHelper output) : base(ActorSystem.Create("SerializationSettingsSpec", ActorSystemSettings), output) { } [Fact] public void Setup_should_be_used_inside_Json_serializer() { - var serializer = (NewtonSoftJsonSerializer) Sys.Serialization.FindSerializerForType(typeof(object)); + var serializer = (NewtonSoftJsonSerializer)Sys.Serialization.FindSerializerForType(typeof(object)); var settings = serializer.Settings; settings.ReferenceLoopHandling.Should().Be(ReferenceLoopHandling.Error); settings.MissingMemberHandling.Should().Be(MissingMemberHandling.Error); @@ -63,11 +63,12 @@ public void Setup_should_be_used_inside_Json_serializer() [Fact] public void Setup_should_not_change_mandatory_settings() { - var serializer = (NewtonSoftJsonSerializer) Sys.Serialization.FindSerializerForType(typeof(object)); + var serializer = (NewtonSoftJsonSerializer)Sys.Serialization.FindSerializerForType(typeof(object)); var settings = serializer.Settings; settings.ContractResolver.Should().BeOfType(); settings.ObjectCreationHandling.Should().Be(ObjectCreationHandling.Replace); - settings.Converters.Any(c => c is NewtonSoftJsonSerializer.SurrogateConverter).Should().Be(true); + settings.Converters.Any(c => c is NewtonSoftJsonSerializer.PrimitiveNumberConverter).Should().Be(true); + settings.Converters.Any(c => c is NewtonSoftJsonSerializer.SurrogatedConverter).Should().Be(true); settings.Converters.Any(c => c is DiscriminatedUnionConverter).Should().Be(true); } } diff --git a/src/core/Akka.Tests/Serialization/NewtonsoftJsonConfigSpec.cs b/src/core/Akka.Tests/Serialization/NewtonsoftJsonConfigSpec.cs index 6eb3b190d23..34bd94d69d2 100644 --- a/src/core/Akka.Tests/Serialization/NewtonsoftJsonConfigSpec.cs +++ b/src/core/Akka.Tests/Serialization/NewtonsoftJsonConfigSpec.cs @@ -26,9 +26,12 @@ public void Json_serializer_should_have_correct_defaults() var serializer = (NewtonSoftJsonSerializer)system.Serialization.FindSerializerForType(typeof(object)); Assert.Equal(TypeNameHandling.All, serializer.Settings.TypeNameHandling); Assert.Equal(PreserveReferencesHandling.Objects, serializer.Settings.PreserveReferencesHandling); - Assert.Equal(2, serializer.Settings.Converters.Count); + Assert.Equal(5, serializer.Settings.Converters.Count); Assert.Contains(serializer.Settings.Converters, x => x is DiscriminatedUnionConverter); + Assert.Contains(serializer.Settings.Converters, x => x is NewtonSoftJsonSerializer.DelegatedObjectConverter); Assert.Contains(serializer.Settings.Converters, x => x is NewtonSoftJsonSerializer.SurrogateConverter); + Assert.Contains(serializer.Settings.Converters, x => x is NewtonSoftJsonSerializer.SurrogatedConverter); + Assert.Contains(serializer.Settings.Converters, x => x is NewtonSoftJsonSerializer.PrimitiveNumberConverter); } } @@ -48,9 +51,12 @@ public void Json_serializer_should_allow_to_setup_custom_flags() var serializer = (NewtonSoftJsonSerializer)system.Serialization.FindSerializerForType(typeof(object)); Assert.Equal(TypeNameHandling.None, serializer.Settings.TypeNameHandling); Assert.Equal(PreserveReferencesHandling.None, serializer.Settings.PreserveReferencesHandling); - Assert.Equal(2, serializer.Settings.Converters.Count); + Assert.Equal(5, serializer.Settings.Converters.Count); Assert.Contains(serializer.Settings.Converters, x => x is DiscriminatedUnionConverter); + Assert.Contains(serializer.Settings.Converters, x => x is NewtonSoftJsonSerializer.DelegatedObjectConverter); Assert.Contains(serializer.Settings.Converters, x => x is NewtonSoftJsonSerializer.SurrogateConverter); + Assert.Contains(serializer.Settings.Converters, x => x is NewtonSoftJsonSerializer.SurrogatedConverter); + Assert.Contains(serializer.Settings.Converters, x => x is NewtonSoftJsonSerializer.PrimitiveNumberConverter); } } @@ -72,9 +78,12 @@ public void Json_serializer_should_allow_to_setup_custom_converters() var serializer = (NewtonSoftJsonSerializer)system.Serialization.FindSerializerForType(typeof(object)); Assert.Equal(TypeNameHandling.All, serializer.Settings.TypeNameHandling); Assert.Equal(PreserveReferencesHandling.Objects, serializer.Settings.PreserveReferencesHandling); - Assert.Equal(4, serializer.Settings.Converters.Count); + Assert.Equal(7, serializer.Settings.Converters.Count); Assert.Contains(serializer.Settings.Converters, x => x is DiscriminatedUnionConverter); + Assert.Contains(serializer.Settings.Converters, x => x is NewtonSoftJsonSerializer.DelegatedObjectConverter); Assert.Contains(serializer.Settings.Converters, x => x is NewtonSoftJsonSerializer.SurrogateConverter); + Assert.Contains(serializer.Settings.Converters, x => x is NewtonSoftJsonSerializer.SurrogatedConverter); + Assert.Contains(serializer.Settings.Converters, x => x is NewtonSoftJsonSerializer.PrimitiveNumberConverter); Assert.Contains(serializer.Settings.Converters, x => x is DummyConverter); Assert.Contains(serializer.Settings.Converters, x => x is DummyConverter2); } @@ -103,7 +112,7 @@ class DummyConverter2 : JsonConverter { public DummyConverter2(ExtendedActorSystem system) { - if (system == null) + if (system == null) throw new ArgumentNullException(nameof(system)); } diff --git a/src/core/Akka/Serialization/DelegatedObjectConverter.cs b/src/core/Akka/Serialization/DelegatedObjectConverter.cs new file mode 100644 index 00000000000..932dce356e8 --- /dev/null +++ b/src/core/Akka/Serialization/DelegatedObjectConverter.cs @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace Akka.Serialization +{ + + public partial class NewtonSoftJsonSerializer + { + internal class DelegatedObjectConverter : JsonConverter + { + private readonly IObjectConverter[] objectConverters; + + public DelegatedObjectConverter(params IObjectConverter[] objectConverters) + { + this.objectConverters = objectConverters; + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(object); + } + + public override bool CanWrite => false; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var deserializedValue = serializer.Deserialize(reader); + + foreach (var objectConverter in objectConverters) + { + if (objectConverter.TryConvert(deserializedValue, out var convertedValue)) + { + return convertedValue; + } + } + + return deserializedValue; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotSupportedException(); + } + } + } +} diff --git a/src/core/Akka/Serialization/IObjectConverter.cs b/src/core/Akka/Serialization/IObjectConverter.cs new file mode 100644 index 00000000000..5b2a4dac594 --- /dev/null +++ b/src/core/Akka/Serialization/IObjectConverter.cs @@ -0,0 +1,18 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +namespace Akka.Serialization +{ + + public partial class NewtonSoftJsonSerializer + { + internal interface IObjectConverter + { + bool TryConvert(object deserializedValue, out object convertedValue); + } + } +} diff --git a/src/core/Akka/Serialization/NewtonSoftJsonSerializer.cs b/src/core/Akka/Serialization/NewtonSoftJsonSerializer.cs index 274c873a4c5..4bcaf9eb7d6 100644 --- a/src/core/Akka/Serialization/NewtonSoftJsonSerializer.cs +++ b/src/core/Akka/Serialization/NewtonSoftJsonSerializer.cs @@ -7,16 +7,15 @@ using System; using System.Collections.Generic; -using System.Globalization; +using System.IO; using System.Linq; using System.Reflection; using System.Text; +using System.Threading; using Akka.Actor; using Akka.Configuration; -using Akka.Util; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; namespace Akka.Serialization @@ -111,9 +110,9 @@ public NewtonSoftJsonSerializerSettings(bool encodeTypeNames, bool preserveObjec /// This is a special that serializes and deserializes javascript objects only. /// These objects need to be in the JavaScript Object Notation (JSON) format. /// - public class NewtonSoftJsonSerializer : Serializer + public partial class NewtonSoftJsonSerializer : Serializer { - private readonly JsonSerializer _serializer; + private readonly ThreadLocal _serializer; /// /// TBD @@ -123,7 +122,7 @@ public class NewtonSoftJsonSerializer : Serializer /// /// TBD /// - public object Serializer { get { return _serializer; } } + public object Serializer => _serializer.Value; /// /// Initializes a new instance of the class. @@ -160,7 +159,7 @@ public NewtonSoftJsonSerializer(ExtendedActorSystem system, NewtonSoftJsonSerial if (system != null) { var settingsSetup = system.Settings.Setup.Get() - .GetOrElse(NewtonSoftJsonSerializerSetup.Create(s => {})); + .GetOrElse(NewtonSoftJsonSerializerSetup.Create(s => { })); settingsSetup.ApplySettings(Settings); } @@ -169,7 +168,12 @@ public NewtonSoftJsonSerializer(ExtendedActorSystem system, NewtonSoftJsonSerial .Select(type => CreateConverter(type, system)) .ToList(); - converters.Add(new SurrogateConverter(this)); + var primitiveNumberConverter = new PrimitiveNumberConverter(); + var surrogateConverter = new SurrogateConverter(system); + converters.Add(new DelegatedObjectConverter(primitiveNumberConverter, surrogateConverter)); + converters.Add(primitiveNumberConverter); + converters.Add(surrogateConverter); + converters.Add(new SurrogatedConverter(system)); converters.Add(new DiscriminatedUnionConverter()); foreach (var converter in converters) @@ -180,7 +184,12 @@ public NewtonSoftJsonSerializer(ExtendedActorSystem system, NewtonSoftJsonSerial Settings.ObjectCreationHandling = ObjectCreationHandling.Replace; //important: if reuse, the serializer will overwrite properties in default references, e.g. Props.DefaultDeploy or Props.noArgs Settings.ContractResolver = new AkkaContractResolver(); - _serializer = JsonSerializer.Create(Settings); + _serializer = new ThreadLocal(() => + { + var serializer = JsonSerializer.CreateDefault(Settings); + serializer.Formatting = Formatting.None; + return serializer; + }); } private static JsonConverter CreateConverter(Type converterType, ExtendedActorSystem actorSystem) @@ -192,7 +201,7 @@ private static JsonConverter CreateConverter(Type converterType, ExtendedActorSy return parameters.Length == 1 && parameters[0].ParameterType == typeof(ExtendedActorSystem); }); - return ctor == null + return ctor == null ? (JsonConverter)Activator.CreateInstance(converterType) : (JsonConverter)Activator.CreateInstance(converterType, actorSystem); } @@ -203,14 +212,10 @@ protected override JsonProperty CreateProperty(MemberInfo member, MemberSerializ { var prop = base.CreateProperty(member, memberSerialization); - if (!prop.Writable) + if (!prop.Writable && member is PropertyInfo property) { - var property = member as PropertyInfo; - if (property != null) - { - var hasPrivateSetter = property.GetSetMethod(true) != null; - prop.Writable = hasPrivateSetter; - } + var hasPrivateSetter = property.GetSetMethod(true) != null; + prop.Writable = hasPrivateSetter; } return prop; @@ -229,9 +234,14 @@ protected override JsonProperty CreateProperty(MemberInfo member, MemberSerializ /// A byte array containing the serialized object public override byte[] ToBinary(object obj) { - string data = JsonConvert.SerializeObject(obj, Formatting.None, Settings); - byte[] bytes = Encoding.UTF8.GetBytes(data); - return bytes; + var stringBuilder = new StringBuilder(256); + var writer = new StringWriter(stringBuilder); + using (var jsonWriter = new JsonTextWriter(writer)) + { + jsonWriter.Formatting = _serializer.Value.Formatting; + _serializer.Value.Serialize(jsonWriter, obj); + } + return Encoding.UTF8.GetBytes(stringBuilder.ToString()); } /// @@ -242,143 +252,10 @@ public override byte[] ToBinary(object obj) /// The object contained in the array public override object FromBinary(byte[] bytes, Type type) { - string data = Encoding.UTF8.GetString(bytes); - object res = JsonConvert.DeserializeObject(data, Settings); - return TranslateSurrogate(res, this, type); - } - - private static object TranslateSurrogate(object deserializedValue, NewtonSoftJsonSerializer parent, Type type) - { - var j = deserializedValue as JObject; - if (j != null) - { - //The JObject represents a special akka.net wrapper for primitives (int,float,decimal) to preserve correct type when deserializing - if (j["$"] != null) - { - var value = j["$"].Value(); - return GetValue(value); - } - - //The JObject is not of our concern, let Json.NET deserialize it. - return j.ToObject(type, parent._serializer); - } - var surrogate = deserializedValue as ISurrogate; - - //The deserialized object is a surrogate, unwrap it - if (surrogate != null) - { - return surrogate.FromSurrogate(parent.system); - } - return deserializedValue; - } - - private static object GetValue(string V) - { - var t = V.Substring(0, 1); - var v = V.Substring(1); - if (t == "I") - return int.Parse(v, NumberFormatInfo.InvariantInfo); - if (t == "F") - return float.Parse(v, NumberFormatInfo.InvariantInfo); - if (t == "M") - return decimal.Parse(v, NumberFormatInfo.InvariantInfo); - - throw new NotSupportedException(); - } - - /// - /// TBD - /// - internal class SurrogateConverter : JsonConverter - { - private readonly NewtonSoftJsonSerializer _parent; - /// - /// TBD - /// - /// TBD - public SurrogateConverter(NewtonSoftJsonSerializer parent) - { - _parent = parent; - } - /// - /// Determines whether this instance can convert the specified object type. - /// - /// Type of the object. - /// true if this instance can convert the specified object type; otherwise, false. - public override bool CanConvert(Type objectType) - { - if (objectType == typeof(int) || objectType == typeof(float) || objectType == typeof(decimal)) - return true; - - if (typeof(ISurrogated).IsAssignableFrom(objectType)) - return true; - - if (objectType == typeof(object)) - return true; - - return false; - } - - /// - /// Reads the JSON representation of the object. - /// - /// The to read from. - /// Type of the object. - /// The existing value of object being read. - /// The calling serializer. - /// The object value. - public override object ReadJson(JsonReader reader, Type objectType, object existingValue, - JsonSerializer serializer) - { - return DeserializeFromReader(reader, serializer, objectType); - } - - private object DeserializeFromReader(JsonReader reader, JsonSerializer serializer, Type objectType) - { - var surrogate = serializer.Deserialize(reader); - return TranslateSurrogate(surrogate, _parent, objectType); - } - - /// - /// Writes the JSON representation of the object. - /// - /// The to write to. - /// The value. - /// The calling serializer. - public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) - { - if (value is int || value is decimal || value is float) - { - writer.WriteStartObject(); - writer.WritePropertyName("$"); - writer.WriteValue(GetString(value)); - writer.WriteEndObject(); - } - else - { - var value1 = value as ISurrogated; - if (value1 != null) - { - var surrogated = value1; - var surrogate = surrogated.ToSurrogate(_parent.system); - serializer.Serialize(writer, surrogate); - } - else - { - serializer.Serialize(writer, value); - } - } - } - - private object GetString(object value) + var reader = new StringReader(Encoding.UTF8.GetString(bytes)); + using (var jsonReader = new JsonTextReader(reader)) { - if (value is int) - return "I" + ((int)value).ToString(NumberFormatInfo.InvariantInfo); - if (value is float) - return "F" + ((float)value).ToString(NumberFormatInfo.InvariantInfo); - if (value is decimal) - return "M" + ((decimal)value).ToString(NumberFormatInfo.InvariantInfo); - throw new NotSupportedException(); + return _serializer.Value.Deserialize(jsonReader, type); } } } diff --git a/src/core/Akka/Serialization/PrimitiveNumberConverter.cs b/src/core/Akka/Serialization/PrimitiveNumberConverter.cs new file mode 100644 index 00000000000..fd01cc98e83 --- /dev/null +++ b/src/core/Akka/Serialization/PrimitiveNumberConverter.cs @@ -0,0 +1,93 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using System.Globalization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Akka.Serialization +{ + + public partial class NewtonSoftJsonSerializer + { + internal class PrimitiveNumberConverter : JsonConverter, IObjectConverter + { + public override bool CanConvert(Type objectType) + { + return objectType == typeof(int) + || objectType == typeof(float) + || objectType == typeof(decimal); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var deserializedValue = serializer.Deserialize(reader); + + if (TryConvert(deserializedValue, out var convertedValue)) + { + return convertedValue; + } + + return deserializedValue; + } + + public bool TryConvert(object deserializedValue, out object convertedValue) + { + if (deserializedValue is JObject jObject + && jObject.TryGetValue("$", out var jToken) + && jToken is JValue jValue + && jValue.Value is string encodedNumberString) + { + var primitiveType = encodedNumberString[0]; + var numberString = encodedNumberString.Substring(1); + switch (primitiveType) + { + case 'I': + convertedValue = int.Parse(numberString, NumberFormatInfo.InvariantInfo); + return true; + case 'F': + convertedValue = float.Parse(numberString, NumberFormatInfo.InvariantInfo); + return true; + case 'M': + convertedValue = decimal.Parse(numberString, NumberFormatInfo.InvariantInfo); + return true; + default: + throw new NotSupportedException($"unsupported primitive type {primitiveType}"); + } + } + + convertedValue = default; + return false; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + string numberString; + switch (value) + { + case int i: + numberString = $"I{i.ToString(NumberFormatInfo.InvariantInfo)}"; + break; + case float f: + numberString = $"F{f.ToString(NumberFormatInfo.InvariantInfo)}"; + break; + case decimal m: + numberString = $"M{m.ToString(NumberFormatInfo.InvariantInfo)}"; + break; + default: + throw new NotSupportedException(); + } + + writer.WriteStartObject(); + writer.WritePropertyName("$"); + writer.WriteValue(numberString); + writer.WriteEndObject(); + } + } + } +} diff --git a/src/core/Akka/Serialization/SurrogateConverter.cs b/src/core/Akka/Serialization/SurrogateConverter.cs new file mode 100644 index 00000000000..0432778e3b2 --- /dev/null +++ b/src/core/Akka/Serialization/SurrogateConverter.cs @@ -0,0 +1,63 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Akka.Actor; +using Akka.Util; +using Newtonsoft.Json; + +namespace Akka.Serialization +{ + public partial class NewtonSoftJsonSerializer + { + internal class SurrogateConverter : JsonConverter, IObjectConverter + { + private readonly ExtendedActorSystem system; + + public SurrogateConverter(ExtendedActorSystem system) + { + this.system = system; + } + + public override bool CanConvert(Type objectType) + { + return typeof(ISurrogate).IsAssignableFrom(objectType); + } + + public override bool CanWrite => false; + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var deserializedValue = serializer.Deserialize(reader); + + if (TryConvert(deserializedValue, out var convertedValue)) + { + return convertedValue; + } + + return deserializedValue; + } + + public bool TryConvert(object deserializedValue, out object convertedValue) + { + if (deserializedValue is ISurrogate surrogate) + { + convertedValue = surrogate.FromSurrogate(system); + return true; + } + + convertedValue = default; + return false; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + throw new NotSupportedException(); + } + } + } +} diff --git a/src/core/Akka/Serialization/SurrogatedConverter.cs b/src/core/Akka/Serialization/SurrogatedConverter.cs new file mode 100644 index 00000000000..4c691c0abe4 --- /dev/null +++ b/src/core/Akka/Serialization/SurrogatedConverter.cs @@ -0,0 +1,55 @@ +//----------------------------------------------------------------------- +// +// Copyright (C) 2009-2021 Lightbend Inc. +// Copyright (C) 2013-2021 .NET Foundation +// +//----------------------------------------------------------------------- + +using System; +using Akka.Actor; +using Akka.Util; +using Newtonsoft.Json; + +namespace Akka.Serialization +{ + public partial class NewtonSoftJsonSerializer + { + internal class SurrogatedConverter : JsonConverter + { + private readonly ExtendedActorSystem system; + + public SurrogatedConverter(ExtendedActorSystem system) + { + this.system = system; + } + + public override bool CanConvert(Type objectType) + { + return typeof(ISurrogated).IsAssignableFrom(objectType); + } + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + var deserializedValue = serializer.Deserialize(reader); + if (deserializedValue is ISurrogate surrogate) + { + return surrogate.FromSurrogate(system); + } + + return deserializedValue; + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + if (value is ISurrogated surrogated) + { + var surrogate = surrogated.ToSurrogate(system); + serializer.Serialize(writer, surrogate); + return; + } + + throw new NotSupportedException(); + } + } + } +}