Skip to content

Commit 3c2f098

Browse files
authored
Add generalized crossplatform support for Hyperion serializer. (#4878)
* Add the groundwork for generalized crossplatform support. * Update Hyperion to 0.10.0 * Convert adapter class to lambda object * Add HyperionSerializerSetup setup class * Add unit test spec * Improve specs, add comments * Add documentation * Add copyright header. * Change readonly fields to readonly properties.
1 parent aed2cbc commit 3c2f098

File tree

7 files changed

+411
-7
lines changed

7 files changed

+411
-7
lines changed

docs/articles/networking/serialization.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,3 +303,80 @@ akka {
303303
}
304304
}
305305
```
306+
307+
## Cross platform serialization compatibility in Hyperion
308+
There are problems that can arise when migrating from old .NET Framework to the new .NET Core standard, mainly because of breaking namespace and assembly name changes between these platforms.
309+
Hyperion implements a generic way of addressing this issue by transforming the names of these incompatible names during deserialization.
310+
311+
There are two ways to set this up, one through the HOCON configuration file, and the other by using the `HyperionSerializerSetup` class.
312+
313+
> [!NOTE]
314+
> Only the first successful name transformation is applied, the rest are ignored.
315+
> If you are matching several similar names, make sure that you order them from the most specific match to the least specific one.
316+
317+
### HOCON
318+
HOCON example:
319+
```
320+
akka.actor.serialization-settings.hyperion.cross-platform-package-name-overrides = {
321+
netfx = [
322+
{
323+
fingerprint = "System.Private.CoreLib,%core%",
324+
rename-from = "System.Private.CoreLib,%core%",
325+
rename-to = "mscorlib,%core%"
326+
}]
327+
netcore = [
328+
{
329+
fingerprint = "mscorlib,%core%",
330+
rename-from = "mscorlib,%core%",
331+
rename-to = "System.Private.CoreLib,%core%"
332+
}]
333+
net = [
334+
{
335+
fingerprint = "mscorlib,%core%",
336+
rename-from = "mscorlib,%core%",
337+
rename-to = "System.Private.CoreLib,%core%"
338+
}]
339+
}
340+
```
341+
342+
In the example above, we're addressing the classic case where the core library name was changed between `mscorlib` in .NET Framework to `System.Private.CoreLib` in .NET Core.
343+
This transform is already included inside Hyperion as the default cross platform support, and used here as an illustration only.
344+
345+
The HOCON configuration section is composed of three object arrays named `netfx`, `netcore`, and `net`, each corresponds, respectively, to .NET Framework, .NET Core, and the new .NET 5.0 and beyond.
346+
The Hyperion serializer will automatically detects the platform it is running on currently and uses the correct array to use inside its deserializer. For example, if Hyperion detects
347+
that it is running under .NET framework, then it will use the `netfx` array to do its deserialization transformation.
348+
349+
The way it works that when the serializer detects that the type name contains the `fingerprint` string, it will replace the string declared in the `rename-from`
350+
property into the string declared in the `rename-to`.
351+
352+
In code, we can write this behaviour as:
353+
```csharp
354+
if(packageName.Contains(fingerprint)) packageName = packageName.Replace(rename-from, rename-to);
355+
```
356+
357+
### HyperionSerializerSetup
358+
359+
This behaviour can also be implemented programatically by providing a `HyperionSerializerSetup` instance during `ActorSystem` creation.
360+
361+
```csharp
362+
#if NETFRAMEWORK
363+
var hyperionSetup = HyperionSerializerSetup.Empty
364+
.WithPackageNameOverrides(new Func<string, string>[]
365+
{
366+
str => str.Contains("System.Private.CoreLib,%core%")
367+
? str.Replace("System.Private.CoreLib,%core%", "mscorlib,%core%") : str
368+
}
369+
#elif NETCOREAPP
370+
var hyperionSetup = HyperionSerializerSetup.Empty
371+
.WithPackageNameOverrides(new Func<string, string>[]
372+
{
373+
str => str.Contains("mscorlib,%core%")
374+
? str.Replace("mscorlib,%core%", "System.Private.CoreLib,%core%") : str
375+
}
376+
#endif
377+
378+
var bootstrap = BootstrapSetup.Create().And(hyperionSetup);
379+
var system = ActorSystem.Create("actorSystem", bootstrap);
380+
```
381+
382+
In the example above, we're using compiler directives to make sure that the correct name transform are used during compilation.

src/contrib/serializers/Akka.Serialization.Hyperion.Tests/HyperionConfigTests.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,61 @@ public void Hyperion_serializer_should_allow_to_setup_custom_types_provider_with
105105
Assert.Equal(typeof(DummyTypesProvider), serializer.Settings.KnownTypesProvider);
106106
}
107107
}
108+
109+
[Fact]
110+
public void Hyperion_serializer_should_read_cross_platform_package_name_override_settings()
111+
{
112+
var config = ConfigurationFactory.ParseString(@"
113+
akka.actor {
114+
serializers.hyperion = ""Akka.Serialization.HyperionSerializer, Akka.Serialization.Hyperion""
115+
serialization-bindings {
116+
""System.Object"" = hyperion
117+
}
118+
serialization-settings.hyperion {
119+
cross-platform-package-name-overrides = {
120+
netfx = [
121+
{
122+
fingerprint = ""a"",
123+
rename-from = ""b"",
124+
rename-to = ""c""
125+
}]
126+
netcore = [
127+
{
128+
fingerprint = ""d"",
129+
rename-from = ""e"",
130+
rename-to = ""f""
131+
}]
132+
net = [
133+
{
134+
fingerprint = ""g"",
135+
rename-from = ""h"",
136+
rename-to = ""i""
137+
}]
138+
}
139+
}
140+
}
141+
");
142+
using (var system = ActorSystem.Create(nameof(HyperionConfigTests), config))
143+
{
144+
var serializer = (HyperionSerializer)system.Serialization.FindSerializerForType(typeof(object));
145+
var overrides = serializer.Settings.PackageNameOverrides.ToList();
146+
Assert.NotEmpty(overrides);
147+
var @override = overrides[0];
148+
149+
#if NET471
150+
Assert.Equal("acc", @override("abc"));
151+
Assert.Equal("bcd", @override("bcd"));
152+
#elif NETCOREAPP3_1
153+
Assert.Equal("dff", @override("def"));
154+
Assert.Equal("efg", @override("efg"));
155+
#elif NET5_0
156+
Assert.Equal("gii", @override("ghi"));
157+
Assert.Equal("hij", @override("hij"));
158+
#else
159+
throw new Exception("Test can not be completed because no proper compiler directive is set for this test build");
160+
#endif
161+
}
162+
}
108163
}
109164

110165
class DummyTypesProvider : IKnownTypesProvider
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
using Akka.Actor;
7+
using Akka.Configuration;
8+
using Akka.TestKit;
9+
using Xunit;
10+
using Xunit.Abstractions;
11+
using FluentAssertions;
12+
13+
namespace Akka.Serialization.Hyperion.Tests
14+
{
15+
public class HyperionSerializerSetupSpec : AkkaSpec
16+
{
17+
private static Config Config
18+
=> ConfigurationFactory.ParseString(@"
19+
akka.actor {
20+
serializers {
21+
hyperion = ""Akka.Serialization.Hyperion, Akka.Serialization.Hyperion""
22+
}
23+
24+
serialization-bindings {
25+
""System.Object"" = hyperion
26+
}
27+
}
28+
");
29+
30+
public HyperionSerializerSetupSpec(ITestOutputHelper output) : base (Config, output)
31+
{ }
32+
33+
[Fact]
34+
public void Setup_should_be_converted_to_settings_correctly()
35+
{
36+
var setup = HyperionSerializerSetup.Empty
37+
.WithPreserveObjectReference(true)
38+
.WithKnownTypeProvider<NoKnownTypes>();
39+
var settings =
40+
new HyperionSerializerSettings(false, false, typeof(DummyTypesProvider), new Func<string, string>[] { s => $"{s}.." });
41+
var appliedSettings = setup.ApplySettings(settings);
42+
43+
appliedSettings.PreserveObjectReferences.Should().BeTrue(); // overriden
44+
appliedSettings.VersionTolerance.Should().BeFalse(); // default
45+
appliedSettings.KnownTypesProvider.Should().Be(typeof(NoKnownTypes)); // overriden
46+
appliedSettings.PackageNameOverrides.Count().Should().Be(1); // from settings
47+
appliedSettings.PackageNameOverrides.First()("a").Should().Be("a..");
48+
}
49+
50+
[Fact]
51+
public void Setup_package_override_should_work()
52+
{
53+
var setup = HyperionSerializerSetup.Empty
54+
.WithPackageNameOverrides(new Func<string, string>[]
55+
{
56+
s => s.Contains("Hyperion.Override")
57+
? s.Replace(".Override", "")
58+
: s
59+
});
60+
61+
var settings = HyperionSerializerSettings.Default;
62+
var appliedSettings = setup.ApplySettings(settings);
63+
64+
var adapter = appliedSettings.PackageNameOverrides.First();
65+
adapter("My.Hyperion.Override").Should().Be("My.Hyperion");
66+
}
67+
}
68+
}

src/contrib/serializers/Akka.Serialization.Hyperion/HyperionSerializer.cs

Lines changed: 72 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,18 @@
66
//-----------------------------------------------------------------------
77

88
using System;
9+
using System.Collections.Generic;
910
using System.IO;
1011
using System.Linq;
1112
using System.Reflection;
13+
using System.Runtime.InteropServices;
1214
using System.Runtime.Serialization;
1315
using Akka.Actor;
1416
using Akka.Configuration;
17+
using Akka.Serialization.Hyperion;
1518
using Akka.Util;
1619
using Hyperion;
20+
using HySerializer = Hyperion.Serializer;
1721

1822
// ReSharper disable once CheckNamespace
1923
namespace Akka.Serialization
@@ -28,7 +32,7 @@ public class HyperionSerializer : Serializer
2832
/// </summary>
2933
public readonly HyperionSerializerSettings Settings;
3034

31-
private readonly Hyperion.Serializer _serializer;
35+
private readonly HySerializer _serializer;
3236

3337
/// <summary>
3438
/// Initializes a new instance of the <see cref="HyperionSerializer"/> class.
@@ -57,7 +61,7 @@ public HyperionSerializer(ExtendedActorSystem system, Config config)
5761
public HyperionSerializer(ExtendedActorSystem system, HyperionSerializerSettings settings)
5862
: base(system)
5963
{
60-
this.Settings = settings;
64+
Settings = settings;
6165
var akkaSurrogate =
6266
Surrogate
6367
.Create<ISurrogated, ISurrogate>(
@@ -66,13 +70,22 @@ public HyperionSerializer(ExtendedActorSystem system, HyperionSerializerSettings
6670

6771
var provider = CreateKnownTypesProvider(system, settings.KnownTypesProvider);
6872

73+
if (system != null)
74+
{
75+
var settingsSetup = system.Settings.Setup.Get<HyperionSerializerSetup>()
76+
.GetOrElse(HyperionSerializerSetup.Empty);
77+
78+
settingsSetup.ApplySettings(Settings);
79+
}
80+
6981
_serializer =
70-
new Hyperion.Serializer(new SerializerOptions(
82+
new HySerializer(new SerializerOptions(
7183
preserveObjectReferences: settings.PreserveObjectReferences,
7284
versionTolerance: settings.VersionTolerance,
7385
surrogates: new[] { akkaSurrogate },
7486
knownTypes: provider.GetKnownTypes(),
75-
ignoreISerializable:true));
87+
ignoreISerializable:true,
88+
packageNameOverrides: settings.PackageNameOverrides));
7689
}
7790

7891
/// <summary>
@@ -156,7 +169,8 @@ public sealed class HyperionSerializerSettings
156169
public static readonly HyperionSerializerSettings Default = new HyperionSerializerSettings(
157170
preserveObjectReferences: true,
158171
versionTolerance: true,
159-
knownTypesProvider: typeof(NoKnownTypes));
172+
knownTypesProvider: typeof(NoKnownTypes),
173+
packageNameOverrides: new List<Func<string, string>>());
160174

161175
/// <summary>
162176
/// Creates a new instance of <see cref="HyperionSerializerSettings"/> using provided HOCON config.
@@ -174,15 +188,42 @@ public sealed class HyperionSerializerSettings
174188
public static HyperionSerializerSettings Create(Config config)
175189
{
176190
if (config.IsNullOrEmpty())
177-
throw ConfigurationException.NullOrEmptyConfig<HyperionSerializerSettings>("akka.serializers.hyperion");
191+
throw ConfigurationException.NullOrEmptyConfig<HyperionSerializerSettings>("akka.actor.serialization-settings.hyperion");
178192

179193
var typeName = config.GetString("known-types-provider", null);
180194
var type = !string.IsNullOrEmpty(typeName) ? Type.GetType(typeName, true) : null;
181195

196+
var framework = RuntimeInformation.FrameworkDescription;
197+
string frameworkKey;
198+
if (framework.Contains(".NET Framework"))
199+
frameworkKey = "netfx";
200+
else if (framework.Contains(".NET Core"))
201+
frameworkKey = "netcore";
202+
else
203+
frameworkKey = "net";
204+
205+
var packageNameOverrides = new List<Func<string, string>>();
206+
var overrideConfigs = config.GetValue($"cross-platform-package-name-overrides.{frameworkKey}");
207+
if (overrideConfigs != null)
208+
{
209+
var configs = overrideConfigs.GetArray().Select(value => value.GetObject());
210+
foreach (var obj in configs)
211+
{
212+
var fingerprint = obj.GetKey("fingerprint").GetString();
213+
var renameFrom = obj.GetKey("rename-from").GetString();
214+
var renameTo = obj.GetKey("rename-to").GetString();
215+
packageNameOverrides.Add(packageName =>
216+
packageName.Contains(fingerprint)
217+
? packageName.Replace(renameFrom, renameTo)
218+
: packageName);
219+
}
220+
}
221+
182222
return new HyperionSerializerSettings(
183223
preserveObjectReferences: config.GetBoolean("preserve-object-references", true),
184224
versionTolerance: config.GetBoolean("version-tolerance", true),
185-
knownTypesProvider: type);
225+
knownTypesProvider: type,
226+
packageNameOverrides: packageNameOverrides);
186227
}
187228

188229
/// <summary>
@@ -207,14 +248,37 @@ public static HyperionSerializerSettings Create(Config config)
207248
/// </summary>
208249
public readonly Type KnownTypesProvider;
209250

251+
/// <summary>
252+
/// A list of lambda functions, used to transform incoming deserialized
253+
/// package names before they are instantiated
254+
/// </summary>
255+
public readonly IEnumerable<Func<string, string>> PackageNameOverrides;
256+
210257
/// <summary>
211258
/// Creates a new instance of a <see cref="HyperionSerializerSettings"/>.
212259
/// </summary>
213260
/// <param name="preserveObjectReferences">Flag which determines if serializer should keep track of references in serialized object graph.</param>
214261
/// <param name="versionTolerance">Flag which determines if field data should be serialized as part of type manifest.</param>
215262
/// <param name="knownTypesProvider">Type implementing <see cref="IKnownTypesProvider"/> to be used to determine a list of types implicitly known by all cooperating serializer.</param>
216263
/// <exception cref="ArgumentException">Raised when `known-types-provider` type doesn't implement <see cref="IKnownTypesProvider"/> interface.</exception>
264+
[Obsolete]
217265
public HyperionSerializerSettings(bool preserveObjectReferences, bool versionTolerance, Type knownTypesProvider)
266+
: this(preserveObjectReferences, versionTolerance, knownTypesProvider, new List<Func<string, string>>())
267+
{ }
268+
269+
/// <summary>
270+
/// Creates a new instance of a <see cref="HyperionSerializerSettings"/>.
271+
/// </summary>
272+
/// <param name="preserveObjectReferences">Flag which determines if serializer should keep track of references in serialized object graph.</param>
273+
/// <param name="versionTolerance">Flag which determines if field data should be serialized as part of type manifest.</param>
274+
/// <param name="knownTypesProvider">Type implementing <see cref="IKnownTypesProvider"/> to be used to determine a list of types implicitly known by all cooperating serializer.</param>
275+
/// <param name="packageNameOverrides">TBD</param>
276+
/// <exception cref="ArgumentException">Raised when `known-types-provider` type doesn't implement <see cref="IKnownTypesProvider"/> interface.</exception>
277+
public HyperionSerializerSettings(
278+
bool preserveObjectReferences,
279+
bool versionTolerance,
280+
Type knownTypesProvider,
281+
IEnumerable<Func<string, string>> packageNameOverrides)
218282
{
219283
knownTypesProvider = knownTypesProvider ?? typeof(NoKnownTypes);
220284
if (!typeof(IKnownTypesProvider).IsAssignableFrom(knownTypesProvider))
@@ -223,6 +287,7 @@ public HyperionSerializerSettings(bool preserveObjectReferences, bool versionTol
223287
PreserveObjectReferences = preserveObjectReferences;
224288
VersionTolerance = versionTolerance;
225289
KnownTypesProvider = knownTypesProvider;
290+
PackageNameOverrides = packageNameOverrides;
226291
}
227292
}
228293
}

0 commit comments

Comments
 (0)