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