diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 6c2c1efc..f7414388 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,3 +1,8 @@ +### 0.11.2 October 7 2021 #### +* Fix exception thrown during deserialization when preserve object reference was turned on + and a surrogate instance was inserted into a collection multiple times. [#264](https://github.com/akkadotnet/Hyperion/pull/264) +* Add support for AggregateException serialization. [#266](https://github.com/akkadotnet/Hyperion/pull/266) + ### 0.11.1 August 17 2021 #### * Add [unsafe deserialization type blacklist](https://github.com/akkadotnet/Hyperion/pull/242) * Bump [Akka version from 1.4.21 to 1.4.23](https://github.com/akkadotnet/Hyperion/pull/246) diff --git a/src/Hyperion.API.Tests/CoreApiSpec.ApproveApi.approved.txt b/src/Hyperion.API.Tests/CoreApiSpec.ApproveApi.approved.txt index a55d7c8f..d5fe90b4 100644 --- a/src/Hyperion.API.Tests/CoreApiSpec.ApproveApi.approved.txt +++ b/src/Hyperion.API.Tests/CoreApiSpec.ApproveApi.approved.txt @@ -36,6 +36,7 @@ namespace Hyperion public object GetDeserializedObject(int id) { } public System.Type GetTypeFromTypeId(int typeId) { } public Hyperion.TypeVersionInfo GetVersionInfo([Hyperion.Internal.NotNull] System.Type type) { } + public void ReplaceOrAddTrackedDeserializedObject([Hyperion.Internal.NotNull] object origin, [Hyperion.Internal.NotNull] object replacement) { } public void TrackDeserializedObject([Hyperion.Internal.NotNull] object obj) { } public void TrackDeserializedType([Hyperion.Internal.NotNull] System.Type type) { } public void TrackDeserializedTypeWithVersion([Hyperion.Internal.NotNull] System.Type type, [Hyperion.Internal.NotNull] Hyperion.TypeVersionInfo versionInfo) { } diff --git a/src/Hyperion.API.Tests/Hyperion.API.Tests.csproj b/src/Hyperion.API.Tests/Hyperion.API.Tests.csproj index 1c1c0aee..47966af4 100644 --- a/src/Hyperion.API.Tests/Hyperion.API.Tests.csproj +++ b/src/Hyperion.API.Tests/Hyperion.API.Tests.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/Hyperion.Akka.Integration.Tests/IntegrationSpec.cs b/src/Hyperion.Akka.Integration.Tests/IntegrationSpec.cs index 7ec81933..aaf7ece9 100644 --- a/src/Hyperion.Akka.Integration.Tests/IntegrationSpec.cs +++ b/src/Hyperion.Akka.Integration.Tests/IntegrationSpec.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Akka.Actor; using Akka.Configuration; using Akka.Serialization; @@ -60,6 +61,32 @@ public void Akka_HyperionSerializer_should_serialize_properly() deserialized.Count.Should().Be(24); } + [Fact] + public void Bugfix263_Akka_HyperionSerializer_should_serialize_ActorPath_list() + { + var actor = Sys.ActorOf(Props.Create()); + var container = new ContainerClass(new List{ actor.Path, actor.Path }); + var serialized = _serializer.ToBinary(container); + var deserialized = _serializer.FromBinary(serialized); + deserialized.Destinations.Count.Should().Be(2); + deserialized.Destinations[0].Should().Be(deserialized.Destinations[1]); + } + + private class MyActor: ReceiveActor + { + + } + + private class ContainerClass + { + public ContainerClass(List destinations) + { + Destinations = destinations; + } + + public List Destinations { get; } + } + private class MyPocoClass { public string Name { get; set; } diff --git a/src/Hyperion.Tests.FSharpData/Hyperion.Tests.FSharpData.fsproj b/src/Hyperion.Tests.FSharpData/Hyperion.Tests.FSharpData.fsproj index 51916107..14373005 100644 --- a/src/Hyperion.Tests.FSharpData/Hyperion.Tests.FSharpData.fsproj +++ b/src/Hyperion.Tests.FSharpData/Hyperion.Tests.FSharpData.fsproj @@ -9,7 +9,7 @@ - + diff --git a/src/Hyperion.Tests/CollectionTests.cs b/src/Hyperion.Tests/CollectionTests.cs index 306028e0..91ecb9de 100644 --- a/src/Hyperion.Tests/CollectionTests.cs +++ b/src/Hyperion.Tests/CollectionTests.cs @@ -15,6 +15,7 @@ using System.Dynamic; using System.IO; using System.Linq; +using FluentAssertions; using Xunit; using Hyperion.SerializerFactories; using Hyperion.ValueSerializers; @@ -415,6 +416,35 @@ public void Issue18() Assert.True(msg.SequenceEqual(deserialized)); } + [Fact] + public void Issue263_CanSerializeArrayOfSurrogate_WhenPreservingObjectReference() + { + var invoked = new List(); + var serializer = new Serializer(new SerializerOptions( + preserveObjectReferences: true, + surrogates: new [] + { + Surrogate.Create( + to => to.ToSurrogate(), + from => { + invoked.Add(from); + return from.FromSurrogate(); + }), + })); + + var objectRef = new SurrogatedClass(5); + var expected = new List { objectRef, objectRef }; + using (var stream = new MemoryStream()) + { + serializer.Serialize(expected, stream); + stream.Position = 0; + var deserialized = serializer.Deserialize>(stream); + deserialized.Count.Should().Be(2); + invoked.Count.Should().Be(1); + ReferenceEquals(deserialized[0], deserialized[1]).Should().BeTrue(); + } + } + #region test classes public class CustomAdd : IEnumerable @@ -451,6 +481,56 @@ public CustomAddRange(IImmutableList inner) IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } + public class SurrogatedClass + { + public class ClassSurrogate + { + public ClassSurrogate(string value) + { + Value = value; + } + + public string Value { get; } + + public SurrogatedClass FromSurrogate() + => new SurrogatedClass(int.Parse(Value)); + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is ClassSurrogate s && s.Value == Value; + } + + public override int GetHashCode() + { + return (Value != null ? Value.GetHashCode() : 0); + } + } + + public SurrogatedClass(int value) + { + Value = value; + } + + public int Value { get; } + + public ClassSurrogate ToSurrogate() + => new ClassSurrogate(Value.ToString()); + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + return obj is SurrogatedClass s && s.Value == Value; + } + + public override int GetHashCode() + { + return Value; + } + } + #endregion [Fact] diff --git a/src/Hyperion.Tests/CustomObjectTests.cs b/src/Hyperion.Tests/CustomObjectTests.cs index f509b51d..4a87e643 100644 --- a/src/Hyperion.Tests/CustomObjectTests.cs +++ b/src/Hyperion.Tests/CustomObjectTests.cs @@ -85,6 +85,44 @@ public void CanSerializeException() Assert.Equal(expected.Message, actual.Message); } + [Fact] + public void CanSerializeAggregateException() + { + Exception ex1; + Exception ex2; + AggregateException expected; + try + { + throw new Exception("hello wire 1"); + } + catch (Exception e) + { + ex1 = e; + } + try + { + throw new Exception("hello wire 2"); + } + catch (Exception e) + { + ex2 = e; + } + try + { + throw new AggregateException("Aggregate", ex1, ex2); + } + catch (AggregateException e) + { + expected = e; + } + Serialize(expected); + Reset(); + var actual = Deserialize(); + Assert.Equal(expected.StackTrace, actual.StackTrace); + Assert.Equal(expected.Message, actual.Message); + Assert.Equal(expected.InnerExceptions.Count, actual.InnerExceptions.Count); + } + [Fact] public void CanSerializePolymorphicObject() { diff --git a/src/Hyperion/DeserializeSession.cs b/src/Hyperion/DeserializeSession.cs index ed7bcadc..8f175aa0 100644 --- a/src/Hyperion/DeserializeSession.cs +++ b/src/Hyperion/DeserializeSession.cs @@ -65,6 +65,15 @@ public object GetDeserializedObject(int id) return _objectById[id]; } + public void ReplaceOrAddTrackedDeserializedObject([NotNull] object origin, [NotNull] object replacement) + { + var index = _objectById.IndexOf(origin); + if (index == -1) + _objectById.Add(origin); + else + _objectById[index] = replacement; + } + public void TrackDeserializedType([NotNull]Type type) { if (_identifierToType == null) diff --git a/src/Hyperion/SerializerFactories/AggregateExceptionSerializerFactory.cs b/src/Hyperion/SerializerFactories/AggregateExceptionSerializerFactory.cs new file mode 100644 index 00000000..2ae7a20d --- /dev/null +++ b/src/Hyperion/SerializerFactories/AggregateExceptionSerializerFactory.cs @@ -0,0 +1,129 @@ +#region copyright +// ----------------------------------------------------------------------- +// +// Copyright (C) 2015-2016 AsynkronIT +// Copyright (C) 2016-2016 Akka.NET Team +// +// ----------------------------------------------------------------------- +#endregion + +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Reflection; +using System.Runtime.Serialization; +using Hyperion.Extensions; +using Hyperion.ValueSerializers; + +namespace Hyperion.SerializerFactories +{ + internal sealed class AggregateExceptionSerializerFactory : ValueSerializerFactory + { + private static readonly TypeInfo ExceptionTypeInfo = typeof(Exception).GetTypeInfo(); + private static readonly TypeInfo AggregateExceptionTypeInfo = typeof(AggregateException).GetTypeInfo(); + private readonly FieldInfo _className; + private readonly FieldInfo _innerException; + private readonly FieldInfo _stackTraceString; + private readonly FieldInfo _remoteStackTraceString; + private readonly FieldInfo _message; + private readonly FieldInfo _innerExceptions; + + public AggregateExceptionSerializerFactory() + { + _className = ExceptionTypeInfo.GetField("_className", BindingFlagsEx.All); + _innerException = ExceptionTypeInfo.GetField("_innerException", BindingFlagsEx.All); + _message = AggregateExceptionTypeInfo.GetField("_message", BindingFlagsEx.All); + _remoteStackTraceString = ExceptionTypeInfo.GetField("_remoteStackTraceString", BindingFlagsEx.All); + _stackTraceString = ExceptionTypeInfo.GetField("_stackTraceString", BindingFlagsEx.All); + _innerExceptions = AggregateExceptionTypeInfo.GetField("m_innerExceptions", BindingFlagsEx.All); + } + + public override bool CanSerialize(Serializer serializer, Type type) => +#if NETSTANDARD16 + false; +#else + AggregateExceptionTypeInfo.IsAssignableFrom(type.GetTypeInfo()); +#endif + + public override bool CanDeserialize(Serializer serializer, Type type) => CanSerialize(serializer, type); + +#if NETSTANDARD16 + // Workaround for CoreCLR where FormatterServices.GetUninitializedObject is not public + private static readonly Func GetUninitializedObject = + (Func) + typeof(string).GetTypeInfo().Assembly.GetType("System.Runtime.Serialization.FormatterServices") + .GetMethod("GetUninitializedObject", BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static) + .CreateDelegate(typeof(Func)); +#else + private static readonly Func GetUninitializedObject = System.Runtime.Serialization.FormatterServices.GetUninitializedObject; +#endif + + public override ValueSerializer BuildSerializer(Serializer serializer, Type type, + ConcurrentDictionary typeMapping) + { +#if !NETSTANDARD1_6 + var exceptionSerializer = new ObjectSerializer(type); + exceptionSerializer.Initialize((stream, session) => + { + var info = new SerializationInfo(type, new FormatterConverter()); + + info.AddValue("ClassName", stream.ReadString(session), typeof (string)); + info.AddValue("Message", stream.ReadString(session), typeof (string)); + info.AddValue("Data", stream.ReadObject(session), typeof (IDictionary)); + info.AddValue("InnerException", stream.ReadObject(session), typeof (Exception)); + info.AddValue("HelpURL", stream.ReadString(session), typeof (string)); + info.AddValue("StackTraceString", stream.ReadString(session), typeof (string)); + info.AddValue("RemoteStackTraceString", stream.ReadString(session), typeof (string)); + info.AddValue("RemoteStackIndex", stream.ReadInt32(session), typeof (int)); + info.AddValue("ExceptionMethod", stream.ReadString(session), typeof (string)); + info.AddValue("HResult", stream.ReadInt32(session)); + info.AddValue("Source", stream.ReadString(session), typeof (string)); + info.AddValue("InnerExceptions", stream.ReadObject(session), typeof (Exception[])); + + return Activator.CreateInstance(type, BindingFlags.NonPublic | BindingFlags.CreateInstance | BindingFlags.Instance, null, new object[]{info, new StreamingContext()}, null); + }, (stream, exception, session) => + { + var info = new SerializationInfo(type, new FormatterConverter()); + var context = new StreamingContext(); + ((AggregateException)exception).GetObjectData(info, context); + + var className = info.GetString("ClassName"); + var message = info.GetString("Message"); + var data = info.GetValue("Data", typeof(IDictionary)); + var innerException = info.GetValue("InnerException", typeof(Exception)); + var helpUrl = info.GetString("HelpURL"); + var stackTraceString = info.GetString("StackTraceString"); + var remoteStackTraceString = info.GetString("RemoteStackTraceString"); + var remoteStackIndex = info.GetInt32("RemoteStackIndex"); + var exceptionMethod = info.GetString("ExceptionMethod"); + var hResult = info.GetInt32("HResult"); + var source = info.GetString("Source"); + var innerExceptions = (Exception[]) info.GetValue("InnerExceptions", typeof(Exception[])); + + StringSerializer.WriteValueImpl(stream, className, session); + StringSerializer.WriteValueImpl(stream, message, session); + stream.WriteObjectWithManifest(data, session); + stream.WriteObjectWithManifest(innerException, session); + StringSerializer.WriteValueImpl(stream, helpUrl, session); + StringSerializer.WriteValueImpl(stream, stackTraceString, session); + StringSerializer.WriteValueImpl(stream, remoteStackTraceString, session); + Int32Serializer.WriteValueImpl(stream, remoteStackIndex, session); + StringSerializer.WriteValueImpl(stream, exceptionMethod, session); + Int32Serializer.WriteValueImpl(stream, hResult, session); + StringSerializer.WriteValueImpl(stream, source, session); + stream.WriteObjectWithManifest(innerExceptions, session); + }); + if (serializer.Options.KnownTypesDict.TryGetValue(type, out var index)) + { + var wrapper = new KnownTypeObjectSerializer(exceptionSerializer, index); + typeMapping.TryAdd(type, wrapper); + } + else + typeMapping.TryAdd(type, exceptionSerializer); + return exceptionSerializer; +#else + return null; +#endif + } + } +} \ No newline at end of file diff --git a/src/Hyperion/SerializerFactories/FromSurrogateSerializerFactory.cs b/src/Hyperion/SerializerFactories/FromSurrogateSerializerFactory.cs index 2409fc5b..1dfd0fe6 100644 --- a/src/Hyperion/SerializerFactories/FromSurrogateSerializerFactory.cs +++ b/src/Hyperion/SerializerFactories/FromSurrogateSerializerFactory.cs @@ -31,7 +31,7 @@ public override ValueSerializer BuildSerializer(Serializer serializer, Type type var surrogate = serializer.Options.Surrogates.FirstOrDefault(s => s.To.GetTypeInfo().IsAssignableFrom(type.GetTypeInfo())); var objectSerializer = new ObjectSerializer(type); // ReSharper disable once PossibleNullReferenceException - var fromSurrogateSerializer = new FromSurrogateSerializer(surrogate.FromSurrogate, objectSerializer); + var fromSurrogateSerializer = new FromSurrogateSerializer(surrogate.FromSurrogate, objectSerializer, serializer.Options.PreserveObjectReferences); typeMapping.TryAdd(type, fromSurrogateSerializer); diff --git a/src/Hyperion/SerializerOptions.cs b/src/Hyperion/SerializerOptions.cs index 59d24754..6b655097 100644 --- a/src/Hyperion/SerializerOptions.cs +++ b/src/Hyperion/SerializerOptions.cs @@ -57,6 +57,7 @@ internal static List> DefaultPackageNameOverrides() new FSharpMapSerializerFactory(), new FSharpListSerializerFactory(), //order is important, try dictionaries before enumerables as dicts are also enumerable + new AggregateExceptionSerializerFactory(), new ExceptionSerializerFactory(), new ImmutableCollectionsSerializerFactory(), new ExpandoObjectSerializerFactory(), diff --git a/src/Hyperion/ValueSerializers/FromSurrogateSerializer.cs b/src/Hyperion/ValueSerializers/FromSurrogateSerializer.cs index 8111cbda..023c98b9 100644 --- a/src/Hyperion/ValueSerializers/FromSurrogateSerializer.cs +++ b/src/Hyperion/ValueSerializers/FromSurrogateSerializer.cs @@ -16,11 +16,16 @@ internal sealed class FromSurrogateSerializer : ValueSerializer { private readonly ValueSerializer _surrogateSerializer; private readonly Func _translator; + private readonly bool _preserveObjectReferences; - public FromSurrogateSerializer(Func translator, ValueSerializer surrogateSerializer) + public FromSurrogateSerializer( + Func translator, + ValueSerializer surrogateSerializer, + bool preserveObjectReferences) { _translator = translator; _surrogateSerializer = surrogateSerializer; + _preserveObjectReferences = preserveObjectReferences; } public override void WriteManifest(Stream stream, SerializerSession session) @@ -37,6 +42,8 @@ public override object ReadValue(Stream stream, DeserializerSession session) { var surrogateValue = _surrogateSerializer.ReadValue(stream, session); var value = _translator(surrogateValue); + if(_preserveObjectReferences) + session.ReplaceOrAddTrackedDeserializedObject(surrogateValue, value); return value; } diff --git a/src/common.props b/src/common.props index 83c58bb9..1395c256 100644 --- a/src/common.props +++ b/src/common.props @@ -21,8 +21,8 @@ Please report any serialization problem that occurs after an upgrade to this ver net5.0 net471 netstandard2.0 - 1.4.23 - 6.0.0 + 1.4.26 + 6.1.0 2.4.1 2.4.3 16.11.0