diff --git a/src/libraries/System.Resources.Extensions/System.Resources.Extensions.sln b/src/libraries/System.Resources.Extensions/System.Resources.Extensions.sln
index ca370f65dd119..cce24913b2415 100644
--- a/src/libraries/System.Resources.Extensions/System.Resources.Extensions.sln
+++ b/src/libraries/System.Resources.Extensions/System.Resources.Extensions.sln
@@ -55,6 +55,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "tools\ref", "{DB5983
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{7FF9B3DF-E383-4487-9ADB-E3BF59CFCE83}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Resources.Extensions.Compat.Tests", "tests\CompatTests\System.Resources.Extensions.Compat.Tests.csproj", "{BD76A85A-8E6F-4D4C-A628-661EFE6999F1}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Resources.Extensions.BinaryFormat.Tests", "tests\BinaryFormatTests\System.Resources.Extensions.BinaryFormat.Tests.csproj", "{4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -503,6 +507,66 @@ Global
{7E29F73C-D2DE-4DA4-B399-F6A2475A11EC}.Checked|arm64.ActiveCfg = Debug|Any CPU
{7E29F73C-D2DE-4DA4-B399-F6A2475A11EC}.Checked|x64.ActiveCfg = Debug|Any CPU
{7E29F73C-D2DE-4DA4-B399-F6A2475A11EC}.Checked|x86.ActiveCfg = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Checked|Any CPU.ActiveCfg = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Checked|Any CPU.Build.0 = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Checked|arm.ActiveCfg = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Checked|arm.Build.0 = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Checked|arm64.ActiveCfg = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Checked|arm64.Build.0 = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Checked|x64.ActiveCfg = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Checked|x64.Build.0 = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Checked|x86.ActiveCfg = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Checked|x86.Build.0 = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Debug|arm.ActiveCfg = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Debug|arm.Build.0 = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Debug|arm64.ActiveCfg = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Debug|arm64.Build.0 = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Debug|x64.Build.0 = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Debug|x86.Build.0 = Debug|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Release|arm.ActiveCfg = Release|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Release|arm.Build.0 = Release|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Release|arm64.ActiveCfg = Release|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Release|arm64.Build.0 = Release|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Release|x64.ActiveCfg = Release|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Release|x64.Build.0 = Release|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Release|x86.ActiveCfg = Release|Any CPU
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1}.Release|x86.Build.0 = Release|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Checked|Any CPU.ActiveCfg = Debug|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Checked|Any CPU.Build.0 = Debug|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Checked|arm.ActiveCfg = Debug|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Checked|arm.Build.0 = Debug|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Checked|arm64.ActiveCfg = Debug|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Checked|arm64.Build.0 = Debug|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Checked|x64.ActiveCfg = Debug|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Checked|x64.Build.0 = Debug|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Checked|x86.ActiveCfg = Debug|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Checked|x86.Build.0 = Debug|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Debug|arm.ActiveCfg = Debug|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Debug|arm.Build.0 = Debug|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Debug|arm64.ActiveCfg = Debug|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Debug|arm64.Build.0 = Debug|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Debug|x64.Build.0 = Debug|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Debug|x86.Build.0 = Debug|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Release|arm.ActiveCfg = Release|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Release|arm.Build.0 = Release|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Release|arm64.ActiveCfg = Release|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Release|arm64.Build.0 = Release|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Release|x64.ActiveCfg = Release|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Release|x64.Build.0 = Release|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Release|x86.ActiveCfg = Release|Any CPU
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -531,6 +595,8 @@ Global
{BE197124-901B-4517-879B-93D0E6684A2C} = {7FF9B3DF-E383-4487-9ADB-E3BF59CFCE83}
{7E29F73C-D2DE-4DA4-B399-F6A2475A11EC} = {DB598308-6179-48EA-A670-0DA6CE5D8340}
{DB598308-6179-48EA-A670-0DA6CE5D8340} = {7FF9B3DF-E383-4487-9ADB-E3BF59CFCE83}
+ {BD76A85A-8E6F-4D4C-A628-661EFE6999F1} = {2195CD2B-EF9E-46A7-B4AA-A2CD31625957}
+ {4D72DCD5-BB24-47ED-9B98-8E0B49A9F314} = {2195CD2B-EF9E-46A7-B4AA-A2CD31625957}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E201426B-999F-48A5-BCFD-3E757AA0E182}
diff --git a/src/libraries/System.Resources.Extensions/src/Resources/Strings.resx b/src/libraries/System.Resources.Extensions/src/Resources/Strings.resx
index 8b3061f262421..f43f1161573ec 100644
--- a/src/libraries/System.Resources.Extensions/src/Resources/Strings.resx
+++ b/src/libraries/System.Resources.Extensions/src/Resources/Strings.resx
@@ -207,4 +207,70 @@
Could not load a converter for type {0}.
+
+ The array contained null(s).
+
+
+ Invalid value: `{0}`.
+
+
+ Unexpected Null Record count.
+
+
+ The serialized array length ({0}) was larger than the configured limit {1}.
+
+
+ {0} Record Type is not supported by design.
+
+
+ Member reference was pointing to a record of unexpected type.
+
+
+ Invalid type name: `{0}`.
+
+
+ Expected the array to be of type {0}, but its element type was {1}.
+
+
+ Only arrays with zero offsets are supported.
+
+
+ Unexpected parser cycle.
+
+
+ Objects could not be deserialized completely.
+
+
+ IObjectReference type '{0}' can only have primitive member data.
+
+
+ The constructor to deserialize an object of type '{0}' was not found.
+
+
+ Could not find field '{0}' data for type '{1}'.
+
+
+ Could not find type '{0}'.
+
+
+ Surrogate must return the same object that was provided in the 'obj' parameter.
+
+
+ Type '{0}' is not marked as serializable.
+
+
+ Invalid type or assembly name: `{0},{1}`.
+
+
+ Stream does not support seeking.
+
+
+ Duplicate member name: `{0}`.
+
+
+ Duplicate Serialization Record Id: `{0}`.
+
+
+ Specified member '{0}' was not of the expected type.
+
\ No newline at end of file
diff --git a/src/libraries/System.Resources.Extensions/src/System.Resources.Extensions.csproj b/src/libraries/System.Resources.Extensions/src/System.Resources.Extensions.csproj
index 061bbaf03974b..74a16e4dc024d 100644
--- a/src/libraries/System.Resources.Extensions/src/System.Resources.Extensions.csproj
+++ b/src/libraries/System.Resources.Extensions/src/System.Resources.Extensions.csproj
@@ -38,15 +38,54 @@ System.Resources.Extensions.PreserializedResourceWriter
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObject.IParseState.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObject.IParseState.cs
new file mode 100644
index 0000000000000..01dc73bc49681
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObject.IParseState.cs
@@ -0,0 +1,22 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.Serialization.BinaryFormat;
+
+namespace System.Resources.Extensions.BinaryFormat;
+
+internal sealed partial class BinaryFormattedObject
+{
+ ///
+ /// Parsing state.
+ ///
+ internal interface IParseState
+ {
+ BinaryReader Reader { get; }
+ IReadOnlyDictionary RecordMap { get; }
+ Options Options { get; }
+ ITypeResolver TypeResolver { get; }
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObject.ITypeResolver.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObject.ITypeResolver.cs
new file mode 100644
index 0000000000000..844ef8bb32fd6
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObject.ITypeResolver.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection.Metadata;
+
+namespace System.Resources.Extensions.BinaryFormat;
+
+internal sealed partial class BinaryFormattedObject
+{
+ ///
+ /// Resolver for types.
+ ///
+ internal interface ITypeResolver
+ {
+ ///
+ /// Resolves the given type name against the specified library.
+ ///
+ [RequiresUnreferencedCode("Calls System.Reflection.Assembly.GetType(String)")]
+ [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
+ Type GetType(TypeName typeName);
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObject.Options.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObject.Options.cs
new file mode 100644
index 0000000000000..131a3807c0ff9
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObject.Options.cs
@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.Formatters;
+
+namespace System.Resources.Extensions.BinaryFormat;
+
+#pragma warning disable SYSLIB0050 // Type or member is obsolete
+
+internal sealed partial class BinaryFormattedObject
+{
+ internal sealed class Options
+ {
+ ///
+ /// How exactly assembly names need to match for deserialization.
+ ///
+ public FormatterAssemblyStyle AssemblyMatching { get; set; } = FormatterAssemblyStyle.Simple;
+
+ ///
+ /// Type name binder.
+ ///
+ public SerializationBinder? Binder { get; set; }
+
+ ///
+ /// Optional type provider.
+ ///
+ public ISurrogateSelector? SurrogateSelector { get; set; }
+
+ ///
+ /// Streaming context.
+ ///
+ public StreamingContext StreamingContext { get; set; } = new(StreamingContextStates.All);
+ }
+}
+
+#pragma warning restore SYSLIB0050 // Type or member is obsolete
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObject.ParseState.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObject.ParseState.cs
new file mode 100644
index 0000000000000..6ca0c290e060f
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObject.ParseState.cs
@@ -0,0 +1,30 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.Serialization.BinaryFormat;
+
+namespace System.Resources.Extensions.BinaryFormat;
+
+internal sealed partial class BinaryFormattedObject
+{
+ ///
+ /// Parsing state for .
+ ///
+ internal sealed class ParseState : IParseState
+ {
+ private readonly BinaryFormattedObject _format;
+
+ public ParseState(BinaryReader reader, BinaryFormattedObject format)
+ {
+ Reader = reader;
+ _format = format;
+ }
+
+ public BinaryReader Reader { get; }
+ public IReadOnlyDictionary RecordMap => _format.RecordMap;
+ public Options Options => _format._options;
+ public ITypeResolver TypeResolver => _format.TypeResolver;
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObject.TypeResolver.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObject.TypeResolver.cs
new file mode 100644
index 0000000000000..693897b78af60
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObject.TypeResolver.cs
@@ -0,0 +1,139 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Reflection;
+using System.Reflection.Metadata;
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.Formatters;
+
+#pragma warning disable SYSLIB0050 // Type or member is obsolete
+
+namespace System.Resources.Extensions.BinaryFormat;
+
+internal sealed partial class BinaryFormattedObject
+{
+ internal sealed class DefaultTypeResolver : ITypeResolver
+ {
+ private readonly FormatterAssemblyStyle _assemblyMatching;
+ private readonly SerializationBinder? _binder;
+
+ private readonly Dictionary _assemblies = [];
+ private readonly Dictionary _types = [];
+
+ internal DefaultTypeResolver(Options options)
+ {
+ _assemblyMatching = options.AssemblyMatching;
+ _binder = options.Binder;
+ }
+
+ ///
+ /// Resolves the given type name against the specified library.
+ ///
+ [RequiresUnreferencedCode("Calls System.Reflection.Assembly.GetType(String)")]
+ [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
+ Type ITypeResolver.GetType(TypeName typeName)
+ {
+ Debug.Assert(typeName.AssemblyName is not null);
+
+ if (_types.TryGetValue(typeName.AssemblyQualifiedName, out Type? cachedType))
+ {
+ return cachedType;
+ }
+
+ if (_binder?.BindToType(typeName.AssemblyName.FullName, typeName.FullName) is Type binderType)
+ {
+ // BinaryFormatter is inconsistent about what caching behavior you get with binders.
+ // It would always cache the last item from the binder, but wouldn't put the result
+ // in the type cache. This could lead to inconsistent results if the binder didn't
+ // always return the same result for a given set of strings. Choosing to always cache
+ // for performance.
+
+ _types[typeName.AssemblyQualifiedName] = binderType;
+ return binderType;
+ }
+
+ if (!_assemblies.TryGetValue(typeName.AssemblyName.FullName, out Assembly? assembly))
+ {
+ AssemblyName assemblyName = typeName.AssemblyName.ToAssemblyName();
+ try
+ {
+ assembly = Assembly.Load(assemblyName);
+ }
+ catch
+ {
+ if (_assemblyMatching != FormatterAssemblyStyle.Simple)
+ {
+ throw;
+ }
+
+ assembly = Assembly.Load(assemblyName.Name!);
+ }
+
+ _assemblies.Add(typeName.AssemblyName.FullName, assembly);
+ }
+
+ Type? type = _assemblyMatching != FormatterAssemblyStyle.Simple
+ ? assembly.GetType(typeName.FullName)
+ : GetSimplyNamedTypeFromAssembly(assembly, typeName);
+
+ _types[typeName.AssemblyQualifiedName] = type ?? throw new SerializationException(SR.Format(SR.Serialization_MissingType, typeName.AssemblyQualifiedName));
+ return type;
+ }
+
+ [RequiresUnreferencedCode("Calls System.Reflection.Assembly.GetType(String, Boolean, Boolean)")]
+ private static Type? GetSimplyNamedTypeFromAssembly(Assembly assembly, TypeName typeName)
+ {
+ // Catching any exceptions that could be thrown from a failure on assembly load
+ // This is necessary, for example, if there are generic parameters that are qualified
+ // with a version of the assembly that predates the one available.
+
+ try
+ {
+ return assembly.GetType(typeName.FullName, throwOnError: false, ignoreCase: false);
+ }
+ catch (TypeLoadException) { }
+ catch (FileNotFoundException) { }
+ catch (FileLoadException) { }
+ catch (BadImageFormatException) { }
+
+ return Type.GetType(typeName.FullName, ResolveSimpleAssemblyName, new TopLevelAssemblyTypeResolver(assembly).ResolveType, throwOnError: false);
+
+ static Assembly? ResolveSimpleAssemblyName(AssemblyName assemblyName)
+ {
+ try
+ {
+ return Assembly.Load(assemblyName);
+ }
+ catch { }
+
+ try
+ {
+ return Assembly.Load(assemblyName.Name!);
+ }
+ catch { }
+
+ return null;
+ }
+ }
+
+ private sealed class TopLevelAssemblyTypeResolver
+ {
+ private readonly Assembly _topLevelAssembly;
+
+ public TopLevelAssemblyTypeResolver(Assembly topLevelAssembly) => _topLevelAssembly = topLevelAssembly;
+
+ [RequiresUnreferencedCode("Calls System.Reflection.Assembly.GetType(String, Boolean, Boolean)")]
+ public Type? ResolveType(Assembly? assembly, string simpleTypeName, bool ignoreCase)
+ {
+ assembly ??= _topLevelAssembly;
+ return assembly.GetType(simpleTypeName, throwOnError: false, ignoreCase);
+ }
+ }
+ }
+}
+
+#pragma warning restore SYSLIB0050 // Type or member is obsolete
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObject.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObject.cs
new file mode 100644
index 0000000000000..35502f066054c
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObject.cs
@@ -0,0 +1,95 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Reflection;
+using System.Runtime.ExceptionServices;
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.BinaryFormat;
+
+namespace System.Resources.Extensions.BinaryFormat;
+
+///
+/// Object model for the binary format put out by BinaryFormatter. It parses and creates a model but does not
+/// instantiate any reference types outside of string.
+///
+///
+///
+/// This is useful for explicitly controlling the rehydration of binary formatted data.
+///
+///
+internal sealed partial class BinaryFormattedObject
+{
+#pragma warning disable SYSLIB0050 // Type or member is obsolete
+ internal static FormatterConverter DefaultConverter { get; } = new();
+#pragma warning restore SYSLIB0050
+
+ private static readonly Options s_defaultOptions = new();
+ private static readonly PayloadOptions s_payloadOptions = new()
+ {
+ UndoTruncatedTypeNames = true // Required for backward compat
+ };
+ private readonly Options _options;
+
+ private ITypeResolver? _typeResolver;
+ private ITypeResolver TypeResolver => _typeResolver ??= new DefaultTypeResolver(_options);
+
+ ///
+ /// Creates by parsing .
+ ///
+ public BinaryFormattedObject(Stream stream, Options? options = null)
+ {
+ _options = options ?? s_defaultOptions;
+
+ try
+ {
+ RootRecord = PayloadReader.Read(stream, out var readonlyRecordMap, options: s_payloadOptions, leaveOpen: true);
+ RecordMap = readonlyRecordMap;
+ }
+ catch (Exception ex) when (ex is ArgumentException or InvalidCastException or ArithmeticException or IOException)
+ {
+ // Make the exception easier to catch, but retain the original stack trace.
+ throw ex.ConvertToSerializationException();
+ }
+ catch (TargetInvocationException ex)
+ {
+ throw ExceptionDispatchInfo.Capture(ex.InnerException!).SourceException.ConvertToSerializationException();
+ }
+ }
+
+ ///
+ /// Deserializes the back to an object.
+ ///
+ [RequiresUnreferencedCode("Ultimately calls Assembly.GetType for type names in the data.")]
+ public object Deserialize()
+ {
+ try
+ {
+ return Deserializer.Deserializer.Deserialize(RootRecord.ObjectId, RecordMap, TypeResolver, _options);
+ }
+ catch (Exception ex) when (ex is ArgumentException or InvalidCastException or ArithmeticException or IOException)
+ {
+ // Make the exception easier to catch, but retain the original stack trace.
+ throw ex.ConvertToSerializationException();
+ }
+ catch (TargetInvocationException ex)
+ {
+ throw ExceptionDispatchInfo.Capture(ex.InnerException!).SourceException.ConvertToSerializationException();
+ }
+ }
+
+ ///
+ /// The Id of the root record of the object graph.
+ ///
+ public SerializationRecord RootRecord { get; }
+
+ ///
+ /// Gets a record by it's identifier. Not all records have identifiers, only ones that
+ /// can be referenced by other records.
+ ///
+ public SerializationRecord this[Id id] => RecordMap[id];
+
+ public IReadOnlyDictionary RecordMap { get; }
+}
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObjectExtensions.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObjectExtensions.cs
new file mode 100644
index 0000000000000..49595e9a5f89d
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/BinaryFormattedObjectExtensions.cs
@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Runtime.Serialization.BinaryFormat;
+
+namespace System.Resources.Extensions.BinaryFormat;
+
+internal static class BinaryFormattedObjectExtensions
+{
+ internal static object GetMemberPrimitiveTypedValue(this SerializationRecord record)
+ {
+ Debug.Assert(record.RecordType is RecordType.MemberPrimitiveTyped or RecordType.BinaryObjectString);
+
+ return record switch
+ {
+ PrimitiveTypeRecord primitive => primitive.Value,
+ PrimitiveTypeRecord primitive => primitive.Value,
+ PrimitiveTypeRecord primitive => primitive.Value,
+ PrimitiveTypeRecord primitive => primitive.Value,
+ PrimitiveTypeRecord primitive => primitive.Value,
+ PrimitiveTypeRecord primitive => primitive.Value,
+ PrimitiveTypeRecord primitive => primitive.Value,
+ PrimitiveTypeRecord primitive => primitive.Value,
+ PrimitiveTypeRecord primitive => primitive.Value,
+ PrimitiveTypeRecord primitive => primitive.Value,
+ PrimitiveTypeRecord primitive => primitive.Value,
+ PrimitiveTypeRecord primitive => primitive.Value,
+ PrimitiveTypeRecord primitive => primitive.Value,
+ PrimitiveTypeRecord primitive => primitive.Value,
+ PrimitiveTypeRecord primitive => primitive.Value,
+ PrimitiveTypeRecord primitive => primitive.Value,
+ PrimitiveTypeRecord primitive => primitive.Value,
+ _ => ((PrimitiveTypeRecord)record).Value
+ };
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ArrayRecordDeserializer.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ArrayRecordDeserializer.cs
new file mode 100644
index 0000000000000..11b8cc1101173
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ArrayRecordDeserializer.cs
@@ -0,0 +1,162 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Runtime.Serialization.BinaryFormat;
+
+namespace System.Resources.Extensions.BinaryFormat.Deserializer;
+
+internal sealed class ArrayRecordDeserializer : ObjectRecordDeserializer
+{
+ private readonly ArrayRecord _arrayRecord;
+ private readonly Type _elementType;
+ private readonly Array _arrayOfClassRecords;
+ private readonly Array _arrayOfT;
+ private readonly int[] _lengths, _indices;
+ private bool _hasFixups, _canIterate;
+
+ [RequiresUnreferencedCode("Calls System.Windows.Forms.BinaryFormat.BinaryFormattedObject.TypeResolver.GetType(TypeName)")]
+ internal ArrayRecordDeserializer(ArrayRecord arrayRecord, IDeserializer deserializer)
+ : base(arrayRecord, deserializer)
+ {
+ // Other array types are handled directly (ArraySinglePrimitive and ArraySingleString).
+ Debug.Assert(arrayRecord.RecordType is not (RecordType.ArraySingleString or RecordType.ArraySinglePrimitive));
+ Debug.Assert(arrayRecord.ArrayType is (BinaryArrayType.Single or BinaryArrayType.Jagged or BinaryArrayType.Rectangular));
+
+ _arrayRecord = arrayRecord;
+ _elementType = deserializer.TypeResolver.GetType(arrayRecord.ElementTypeName);
+ Type expectedArrayType = arrayRecord.ArrayType switch
+ {
+ BinaryArrayType.Rectangular => _elementType.MakeArrayType(arrayRecord.Rank),
+ _ => _elementType.MakeArrayType()
+ };
+ // Tricky part: for arrays of classes/structs the following record allocates and array of class records
+ // (because the payload reader can not load types, instantiate objects and rehydrate them)
+ _arrayOfClassRecords = arrayRecord.GetArray(expectedArrayType);
+ // Now we need to create an array of the same length, but of a different, exact type
+ Type elementType = _arrayOfClassRecords.GetType();
+ while (elementType.IsArray)
+ {
+ elementType = elementType.GetElementType()!;
+ }
+
+ _lengths = arrayRecord.Lengths.ToArray();
+ Object = _arrayOfT = Array.CreateInstance(_elementType, _lengths);
+ _indices = new int[_lengths.Length];
+ _canIterate = _arrayOfT.Length > 0;
+ }
+
+ internal override Id Continue()
+ {
+ int[] indices = _indices;
+ int[] lengths = _lengths;
+
+ while (_canIterate)
+ {
+ (object? memberValue, Id reference) = UnwrapMemberValue(_arrayOfClassRecords.GetValue(indices));
+
+ if (s_missingValueSentinel == memberValue)
+ {
+ // Record has not been encountered yet, need to pend iteration.
+ return reference;
+ }
+
+ if (memberValue is not null && DoesValueNeedUpdated(memberValue, reference))
+ {
+ // Need to track a fixup for this index.
+ _hasFixups = true;
+ Deserializer.PendValueUpdater(new ArrayUpdater(_arrayRecord.ObjectId, reference, indices.ToArray()));
+ }
+
+ _arrayOfT.SetValue(memberValue, indices);
+
+ int dimension = indices.Length - 1;
+ while (dimension >= 0)
+ {
+ indices[dimension]++;
+ if (indices[dimension] < lengths[dimension])
+ {
+ break;
+ }
+
+ indices[dimension] = 0;
+ dimension--;
+ }
+
+ if (dimension < 0)
+ {
+ _canIterate = false;
+ }
+ }
+
+ // No more missing member refs.
+
+ if (!_hasFixups)
+ {
+ Deserializer.CompleteObject(_arrayRecord.ObjectId);
+ }
+
+ return Id.Null;
+ }
+
+ internal static Array GetArraySinglePrimitive(SerializationRecord record) => record switch
+ {
+ ArrayRecord primitiveArray => primitiveArray.GetArray(),
+ ArrayRecord primitiveArray => primitiveArray.GetArray(),
+ ArrayRecord primitiveArray => primitiveArray.GetArray(),
+ ArrayRecord primitiveArray => primitiveArray.GetArray(),
+ ArrayRecord primitiveArray => primitiveArray.GetArray(),
+ ArrayRecord primitiveArray => primitiveArray.GetArray(),
+ ArrayRecord primitiveArray => primitiveArray.GetArray(),
+ ArrayRecord primitiveArray => primitiveArray.GetArray(),
+ ArrayRecord primitiveArray => primitiveArray.GetArray(),
+ ArrayRecord primitiveArray => primitiveArray.GetArray(),
+ ArrayRecord primitiveArray => primitiveArray.GetArray(),
+ ArrayRecord primitiveArray => primitiveArray.GetArray(),
+ ArrayRecord primitiveArray => primitiveArray.GetArray(),
+ ArrayRecord primitiveArray => primitiveArray.GetArray(),
+ ArrayRecord primitiveArray => primitiveArray.GetArray(),
+ _ => throw new NotSupportedException(),
+ };
+
+ [RequiresUnreferencedCode("Calls System.Windows.Forms.BinaryFormat.BinaryFormattedObject.TypeResolver.GetType(TypeName)")]
+ internal static Array? GetSimpleBinaryArray(ArrayRecord arrayRecord, BinaryFormattedObject.ITypeResolver typeResolver)
+ {
+ if (arrayRecord.ArrayType is not (BinaryArrayType.Single or BinaryArrayType.Jagged or BinaryArrayType.Rectangular))
+ {
+ throw new NotSupportedException(SR.NotSupported_NonZeroOffsets);
+ }
+
+ Type arrayRecordElementType = typeResolver.GetType(arrayRecord.ElementTypeName);
+ Type elementType = arrayRecordElementType;
+ while (elementType.IsArray)
+ {
+ elementType = elementType.GetElementType()!;
+ }
+
+ if (!(HasBuiltInSupport(elementType)
+ || (Nullable.GetUnderlyingType(elementType) is Type nullable && HasBuiltInSupport(nullable))))
+ {
+ return null;
+ }
+
+ Type expectedArrayType = arrayRecord.ArrayType switch
+ {
+ BinaryArrayType.Rectangular => arrayRecordElementType.MakeArrayType(arrayRecord.Rank),
+ _ => arrayRecordElementType.MakeArrayType()
+ };
+
+ return arrayRecord.GetArray(expectedArrayType);
+
+ static bool HasBuiltInSupport(Type elementType)
+ => elementType == typeof(string)
+ || elementType == typeof(bool) || elementType == typeof(byte) || elementType == typeof(sbyte)
+ || elementType == typeof(char) || elementType == typeof(short) || elementType == typeof(ushort)
+ || elementType == typeof(int) || elementType == typeof(uint)
+ || elementType == typeof(long) || elementType == typeof(ulong)
+ || elementType == typeof(float) || elementType == typeof(double) || elementType == typeof(decimal)
+ || elementType == typeof(DateTime) || elementType == typeof(TimeSpan);
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ArrayUpdater.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ArrayUpdater.cs
new file mode 100644
index 0000000000000..8cd8326278103
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ArrayUpdater.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Resources.Extensions.BinaryFormat.Deserializer;
+
+internal sealed class ArrayUpdater : ValueUpdater
+{
+ private readonly int[] _indices;
+
+ internal ArrayUpdater(int objectId, int valueId, int[] indices) : base(objectId, valueId)
+ {
+ _indices = indices;
+ }
+
+ internal override void UpdateValue(IDictionary objects)
+ {
+ object value = objects[ValueId];
+ Array array = (Array)objects[ObjectId];
+ array.SetValue(value, _indices);
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ClassRecordDeserializer.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ClassRecordDeserializer.cs
new file mode 100644
index 0000000000000..a076db8262b77
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ClassRecordDeserializer.cs
@@ -0,0 +1,101 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.BinaryFormat;
+
+namespace System.Resources.Extensions.BinaryFormat.Deserializer;
+
+#pragma warning disable SYSLIB0050 // Type or member is obsolete
+
+///
+/// Base class for deserializing s.
+///
+internal abstract class ClassRecordDeserializer : ObjectRecordDeserializer
+{
+ private readonly bool _onlyAllowPrimitives;
+
+ private protected ClassRecordDeserializer(ClassRecord classRecord, object @object, IDeserializer deserializer)
+ : base(classRecord, deserializer)
+ {
+ Object = @object;
+
+ // We want to be able to complete IObjectReference without having to evaluate their dependencies
+ // for circular references. See ValidateNewMemberObjectValue below for more.
+ _onlyAllowPrimitives = @object is IObjectReference;
+ }
+
+ [RequiresUnreferencedCode("Calls System.Windows.Forms.BinaryFormat.BinaryFormattedObject.TypeResolver.GetType(TypeName)")]
+ internal static ObjectRecordDeserializer Create(ClassRecord classRecord, IDeserializer deserializer)
+ {
+ Type type = deserializer.TypeResolver.GetType(classRecord.TypeName);
+ Id id = classRecord.ObjectId;
+
+ ISerializationSurrogate? surrogate = deserializer.GetSurrogate(type);
+
+ if (!type.IsSerializable && surrogate is null)
+ {
+ // SurrogateSelectors allow populating types that are not marked as serializable.
+ throw new SerializationException(SR.Format(SR.Serialization_TypeNotSerializable, type));
+ }
+
+ object @object =
+#if NETCOREAPP
+ RuntimeHelpers.GetUninitializedObject(type);
+#else
+ Runtime.Serialization.FormatterServices.GetUninitializedObject(type);
+#endif
+
+ // Invoke any OnDeserializing methods.
+ SerializationEvents.GetOnDeserializingForType(type, @object)?.Invoke(deserializer.Options.StreamingContext);
+
+ ObjectRecordDeserializer? recordDeserializer;
+
+ if (surrogate is not null || typeof(ISerializable).IsAssignableFrom(type))
+ {
+ recordDeserializer = new ClassRecordSerializationInfoDeserializer(classRecord, @object, type, surrogate, deserializer);
+ }
+ else
+ {
+ // Directly set fields for non-ISerializable types.
+ recordDeserializer = new ClassRecordFieldInfoDeserializer(classRecord, @object, type, deserializer);
+ }
+
+ return recordDeserializer;
+ }
+
+ private protected override void ValidateNewMemberObjectValue(object value)
+ {
+ if (!_onlyAllowPrimitives)
+ {
+ return;
+ }
+
+ // The goal with this restriction is to know definitively that we can complete the contianing object when we
+ // finish with it's members, even if it is going to be replaced with another instance (as IObjectReference does).
+ // If there are no reference types we know that there is no way for references to this object getting it in an
+ // in an unconverted state or converted with uncompleted state (due to some direct or indirect reference from
+ // this object).
+ //
+ // If we wanted support to be fully open-ended we would have queue completion along with pending SerializationInfo
+ // objects to rehydrate in the proper order (depth-first) and reject any case where the object is involved in
+ // a cycle.
+
+ Type type = value.GetType();
+ if (type.IsArray)
+ {
+ type = type.GetElementType()!;
+ }
+
+ bool primitive = type.IsPrimitive || type.IsEnum || type == typeof(string);
+ if (!primitive)
+ {
+ throw new SerializationException(SR.Format(SR.Serialization_IObjectReferenceOnlyPrimivite, type));
+ }
+ }
+}
+
+#pragma warning restore SYSLIB0050 // Type or member is obsolete
+
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ClassRecordFieldInfoDeserializer.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ClassRecordFieldInfoDeserializer.cs
new file mode 100644
index 0000000000000..87a3ee4bc5e89
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ClassRecordFieldInfoDeserializer.cs
@@ -0,0 +1,99 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.BinaryFormat;
+using System.Runtime.Serialization.Formatters;
+
+namespace System.Resources.Extensions.BinaryFormat.Deserializer;
+
+#pragma warning disable SYSLIB0050 // Type or member is obsolete
+
+///
+/// Deserializer for s that directly set fields.
+///
+internal sealed class ClassRecordFieldInfoDeserializer : ClassRecordDeserializer
+{
+ private readonly Runtime.Serialization.BinaryFormat.ClassRecord _classRecord;
+ private readonly MemberInfo[] _fieldInfo;
+ private int _currentFieldIndex;
+ private readonly bool _isValueType;
+ private bool _hasFixups;
+
+ internal ClassRecordFieldInfoDeserializer(
+ Runtime.Serialization.BinaryFormat.ClassRecord classRecord,
+ object @object,
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
+ Type type,
+ IDeserializer deserializer)
+ : base(classRecord, @object, deserializer)
+ {
+ _classRecord = classRecord;
+#pragma warning disable IL2067 // GetSerializableMembers is not attributed correctly. Should just be fields.
+ _fieldInfo = Runtime.Serialization.FormatterServices.GetSerializableMembers(type);
+#pragma warning restore IL2067
+ _isValueType = type.IsValueType;
+ }
+
+ internal override Id Continue()
+ {
+ // When directly setting fields we need to populate fields with primitive types before we
+ // can add the object to the deserialized object list to handle value types. This ensures
+ // partialially filled boxed value types in the collection are assigned (and unboxed) in
+ // this path (non-ISerializable) with nothing directly pending other than reference types.
+
+ Debug.Assert(_fieldInfo is not null);
+
+ // Note that while fields must have member data, fields are not required for all member data.
+
+ while (_currentFieldIndex < _fieldInfo.Length)
+ {
+ // FormatterServices *never* returns anything but fields.
+ FieldInfo field = (FieldInfo)_fieldInfo[_currentFieldIndex];
+ if (!_classRecord.HasMember(field.Name))
+ {
+ if (Deserializer.Options.AssemblyMatching == FormatterAssemblyStyle.Simple
+ || field.GetCustomAttribute() is not null)
+ {
+ _currentFieldIndex++;
+ continue;
+ }
+
+ throw new SerializationException(SR.Format(SR.Serialization_MissingField, field.Name, field.DeclaringType!.Name));
+ }
+
+ (object? memberValue, Id reference) = UnwrapMemberValue(_classRecord.GetRawValue(field.Name));
+ if (s_missingValueSentinel == memberValue)
+ {
+ // Record has not been encountered yet, need to pend iteration.
+ return reference;
+ }
+
+ field.SetValue(Object, memberValue);
+
+ if (memberValue is not null && DoesValueNeedUpdated(memberValue, reference))
+ {
+ // Need to track a fixup for this field.
+ _hasFixups = true;
+ Deserializer.PendValueUpdater(new FieldValueUpdater(_classRecord.ObjectId, reference, field));
+ }
+
+ _currentFieldIndex++;
+ }
+
+ if (!_hasFixups || !_isValueType)
+ {
+ // We can be used for completion even with fixups if we're not a value type as our fixups won't need to be
+ // copied to propogate them. Note that surrogates cannot replace our created instance for reference types.
+ Deserializer.CompleteObject(_classRecord.ObjectId);
+ }
+
+ // No more missing member refs.
+ return Id.Null;
+ }
+}
+
+#pragma warning restore SYSLIB0050 // Type or member is obsolete
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ClassRecordSerializationInfoDeserializer.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ClassRecordSerializationInfoDeserializer.cs
new file mode 100644
index 0000000000000..71053d04cebc1
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ClassRecordSerializationInfoDeserializer.cs
@@ -0,0 +1,92 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.BinaryFormat;
+
+namespace System.Resources.Extensions.BinaryFormat.Deserializer;
+
+#pragma warning disable SYSLIB0050 // Type or member is obsolete
+
+///
+/// Deserializer for s that use to initialize class state.
+///
+///
+///
+/// This is used either because the class implements or because a surrogate was used.
+///
+///
+internal sealed class ClassRecordSerializationInfoDeserializer : ClassRecordDeserializer
+{
+ private readonly Runtime.Serialization.BinaryFormat.ClassRecord _classRecord;
+ private readonly SerializationInfo _serializationInfo;
+ private readonly ISerializationSurrogate? _surrogate;
+ private readonly IEnumerator _memberNamesIterator;
+ private bool _canIterate;
+
+ internal ClassRecordSerializationInfoDeserializer(
+ Runtime.Serialization.BinaryFormat.ClassRecord classRecord,
+ object @object,
+ Type type,
+ ISerializationSurrogate? surrogate,
+ IDeserializer deserializer) : base(classRecord, @object, deserializer)
+ {
+ _classRecord = classRecord;
+ _surrogate = surrogate;
+ _serializationInfo = new(type, BinaryFormattedObject.DefaultConverter);
+ _memberNamesIterator = _classRecord.MemberNames.GetEnumerator();
+ _canIterate = _memberNamesIterator.MoveNext(); // start the iterator
+ }
+
+ internal override Id Continue()
+ {
+ if (_canIterate)
+ {
+ do
+ {
+ string memberName = _memberNamesIterator.Current;
+ (object? memberValue, Id reference) = UnwrapMemberValue(_classRecord.GetRawValue(memberName));
+
+ if (s_missingValueSentinel == memberValue)
+ {
+ // Record has not been encountered yet, need to pend iteration.
+ return reference;
+ }
+
+ if (memberValue is not null && DoesValueNeedUpdated(memberValue, reference))
+ {
+ Deserializer.PendValueUpdater(new SerializationInfoValueUpdater(
+ _classRecord.ObjectId,
+ reference,
+ _serializationInfo,
+ memberName));
+ }
+
+ _serializationInfo.AddValue(memberName, memberValue);
+ }
+ while (_memberNamesIterator.MoveNext());
+
+ _canIterate = false;
+ }
+
+ // We can't complete these in the same way we do with direct field sets as user code can dereference the
+ // reference type members from the SerializationInfo that aren't fully completed (due to cycles). With direct
+ // field sets it doesn't matter if the referenced object isn't fully completed. Waiting until the graph is
+ // fully parsed to allow cycles the best chance to resolve as much as possible without having to walk the
+ // entire graph from this point to make a determination.
+ //
+ // The same issue applies to "complete" events, which is why we pend them as well.
+ //
+ // If we were confident that there were no cycles in the graph to this point we could apply directly
+ // if there were no pending value types (which should also not happen if there are no cycles).
+
+ PendingSerializationInfo pending = new(_classRecord.ObjectId, _serializationInfo, _surrogate);
+ Deserializer.PendSerializationInfo(pending);
+
+ // No more missing member refs.
+ return Id.Null;
+ }
+}
+
+#pragma warning restore SYSLIB0050 // Type or member is obsolete
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/Deserializer.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/Deserializer.cs
new file mode 100644
index 0000000000000..b553b2367a7d4
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/Deserializer.cs
@@ -0,0 +1,409 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.BinaryFormat;
+
+namespace System.Resources.Extensions.BinaryFormat.Deserializer;
+
+#pragma warning disable SYSLIB0050 // Type or member is obsolete
+
+///
+/// General binary format deserializer.
+///
+///
+///
+/// This has some constraints over the BinaryFormatter. Notably it does not support all
+/// usages or surrogates that replace object instances. This greatly simplifies the deserialization. It also does not
+/// allow offset arrays (arrays that have lower bounds other than zero) or multidimensional arrays that have more
+/// than elements.
+///
+///
+/// This deserializer ensures that all value types are assigned to fields or populated in
+/// callbacks with their final state, throwing if that is impossible to attain due to graph cycles or data corruption.
+/// The value type instance may contain references to uncompleted reference types when there are cycles in the graph.
+/// In general it is risky to dereference reference types in constructors or in
+/// call backs if there is any risk of the objects enabling a cycle.
+///
+///
+/// If you need to dereference reference types in waiting for final state by
+/// implementing or is the safer way to
+/// do so. This deserializer does not fire completed events until the entire graph has been deserialized. If a
+/// surrogate ( ) needs to dereference with potential cycles it would require
+/// tracking instances by stashing them in a provided to handle after invoking the
+/// deserializer.
+///
+///
+///
+/// makes deserializing difficult as you don't know the final type until you've finished
+/// populating the serialized type. If is involved and you have a cycle you may never
+/// be able to complete the deserialization as the reference type values in the can't
+/// get the final object.
+///
+/// is really the only practical way to represent singletons. A common pattern is to
+/// nest an object in an object. Specifying the nested
+/// type when is called by invoking
+/// will get that type info serialized into the stream.
+///
+internal sealed partial class Deserializer : IDeserializer
+{
+ private readonly IReadOnlyDictionary _recordMap;
+ private readonly BinaryFormattedObject.ITypeResolver _typeResolver;
+ BinaryFormattedObject.ITypeResolver IDeserializer.TypeResolver => _typeResolver;
+
+ ///
+ private BinaryFormattedObject.Options Options { get; }
+ BinaryFormattedObject.Options IDeserializer.Options => Options;
+
+ ///
+ private readonly Dictionary _deserializedObjects = [];
+ IDictionary IDeserializer.DeserializedObjects => _deserializedObjects;
+
+ // Surrogate cache.
+ private readonly Dictionary? _surrogates;
+
+ // Queue of SerializationInfo objects that need to be applied. These are in depth first order,
+ // if there are no cycles in the graph this ensures that all objects are available when the
+ // SerializationInfo is applied.
+ //
+ // We also keep a hashset for quickly checking to make sure we do not complete objects before we
+ // actually apply the SerializationInfo. While we could mark them in the incomplete dependencies
+ // dictionary, to do so we'd need to know if any referenced object is going to get to this state
+ // even if it hasn't finished parsing, which isn't easy to do with cycles involved.
+ private Queue? _pendingSerializationInfo;
+ private HashSet? _pendingSerializationInfoIds;
+
+ // Keeping a separate stack for ids for fast infinite loop checks.
+ private readonly Stack _parseStack = [];
+ private readonly Stack _parserStack = [];
+
+ ///
+ private readonly HashSet _incompleteObjects = [];
+ public HashSet IncompleteObjects => _incompleteObjects;
+
+ // For a given object id, the set of ids that it is waiting on to complete.
+ private Dictionary>? _incompleteDependencies;
+
+ // The pending value updaters. Scanned each time an object is completed.
+ private HashSet? _pendingUpdates;
+
+ // Kept as a field to avoid allocating a new one every time we complete objects.
+ private readonly Queue _pendingCompletions = [];
+
+ private readonly Id _rootId;
+
+ // We group individual object events here to fire them all when we complete the graph.
+ private event Action? OnDeserialization;
+ private event Action? OnDeserialized;
+
+ private Deserializer(
+ Id rootId,
+ IReadOnlyDictionary recordMap,
+ BinaryFormattedObject.ITypeResolver typeResolver,
+ BinaryFormattedObject.Options options)
+ {
+ _rootId = rootId;
+ _recordMap = recordMap;
+ _typeResolver = typeResolver;
+ Options = options;
+
+ if (Options.SurrogateSelector is not null)
+ {
+ _surrogates = [];
+ }
+ }
+
+ ///
+ /// Deserializes the object graph for the given and .
+ ///
+ [RequiresUnreferencedCode("Calls System.Windows.Forms.BinaryFormat.Deserializer.Deserializer.Deserialize()")]
+ internal static object Deserialize(
+ Id rootId,
+ IReadOnlyDictionary recordMap,
+ BinaryFormattedObject.ITypeResolver typeResolver,
+ BinaryFormattedObject.Options options)
+ {
+ var deserializer = new Deserializer(rootId, recordMap, typeResolver, options);
+ return deserializer.Deserialize();
+ }
+
+ [RequiresUnreferencedCode("Calls System.Windows.Forms.BinaryFormat.Deserializer.Deserializer.DeserializeRoot(Id)")]
+ private object Deserialize()
+ {
+ DeserializeRoot(_rootId);
+
+ // Complete all pending SerializationInfo objects.
+ int pendingCount = _pendingSerializationInfo?.Count ?? 0;
+ while (_pendingSerializationInfo is not null && _pendingSerializationInfo.Count > 0)
+ {
+ PendingSerializationInfo? pending = _pendingSerializationInfo.Dequeue();
+
+ // Using pendingCount to only requeue on the first pass.
+ if (--pendingCount >= 0
+ && _pendingSerializationInfo.Count != 0
+ && _incompleteDependencies is not null
+ && _incompleteDependencies.TryGetValue(pending.ObjectId, out HashSet? dependencies))
+ {
+ // We can get here with nested ISerializable value types.
+
+ // Hopefully another pass will complete this.
+ if (dependencies.Count > 0)
+ {
+ _pendingSerializationInfo.Enqueue(pending);
+ continue;
+ }
+
+ Debug.Fail("Completed dependencies should have been removed from the dictionary.");
+ }
+
+ // All _pendingSerializationInfo objects are considered incomplete.
+ pending.Populate(_deserializedObjects, Options.StreamingContext);
+ _pendingSerializationInfoIds?.Remove(pending.ObjectId);
+ ((IDeserializer)this).CompleteObject(pending.ObjectId);
+ }
+
+ if (_incompleteObjects.Count > 0 || (_pendingUpdates is not null && _pendingUpdates.Count > 0))
+ {
+ // This should never happen outside of corrupted data.
+ throw new SerializationException(SR.Serialization_Incomplete);
+ }
+
+ // Notify [OnDeserialized] instance methods for all relevant deserialized objects,
+ // then callback IDeserializationCallback on all objects that implement it.
+ OnDeserialized?.Invoke(Options.StreamingContext);
+ OnDeserialization?.Invoke(null);
+
+ return _deserializedObjects[_rootId];
+ }
+
+ [RequiresUnreferencedCode("Calls DeserializeNew(Id)")]
+ private void DeserializeRoot(Id rootId)
+ {
+ object root = DeserializeNew(rootId);
+ if (root is not ObjectRecordDeserializer parser)
+ {
+ return;
+ }
+
+ _parseStack.Push(rootId);
+ _parserStack.Push(parser);
+
+ while (_parserStack.Count > 0)
+ {
+ ObjectRecordDeserializer? currentParser = _parserStack.Pop();
+ int currentId = _parseStack.Pop();
+ Debug.Assert(currentId == currentParser.ObjectRecord.ObjectId);
+
+ Id requiredId;
+ while (!(requiredId = currentParser.Continue()).IsNull)
+ {
+ // Beside ObjectRecordDeserializer, DeserializeNew can return a raw value like int, string or an array.
+ if (DeserializeNew(requiredId) is ObjectRecordDeserializer requiredParser)
+ {
+ // The required object is not complete.
+
+ if (_parseStack.Contains(requiredId))
+ {
+ // All objects should be available before they're asked for a second time.
+ throw new SerializationException(SR.Serialization_Cycle);
+ }
+
+ // Push our current parser.
+ _parseStack.Push(currentId);
+ _parserStack.Push(currentParser);
+
+ // Push the required parser so we can complete it.
+ _parseStack.Push(requiredId);
+ _parserStack.Push(requiredParser);
+
+ break;
+ }
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ [RequiresUnreferencedCode("Calls System.Windows.Forms.BinaryFormat.Deserializer.ObjectRecordParser.Create(Id, IRecord, IDeserializer)")]
+ object DeserializeNew(Id id)
+ {
+ // Strings, string arrays, and primitive arrays can be completed without creating a
+ // parser object. Single primitives don't normally show up as records unless they are top
+ // level or are boxed into an interface reference. Checking for these requires costly
+ // string matches and as such we'll just create the parser object.
+
+ SerializationRecord record = _recordMap[id];
+
+ object? value = record.RecordType switch
+ {
+ RecordType.BinaryObjectString => ((PrimitiveTypeRecord)record).Value,
+ RecordType.MemberPrimitiveTyped => record.GetMemberPrimitiveTypedValue(),
+ RecordType.ArraySingleString => ((ArrayRecord)record).GetArray(),
+ RecordType.ArraySinglePrimitive => ArrayRecordDeserializer.GetArraySinglePrimitive(record),
+ RecordType.BinaryArray => ArrayRecordDeserializer.GetSimpleBinaryArray((ArrayRecord)record, _typeResolver),
+ _ => null
+ };
+
+ if (value is not null)
+ {
+ _deserializedObjects.Add(record.ObjectId, value);
+ return value;
+ }
+
+ // Not a simple case, need to do a full deserialization of the record.
+ _incompleteObjects.Add(id);
+
+ var deserializer = ObjectRecordDeserializer.Create(record, this);
+
+ // Add the object as soon as possible to support circular references.
+ _deserializedObjects.Add(id, deserializer.Object);
+ return deserializer;
+ }
+ }
+
+ ISerializationSurrogate? IDeserializer.GetSurrogate(Type type)
+ {
+ // If we decide not to cache, this method could be moved to the callsite.
+
+ if (_surrogates is null)
+ {
+ return null;
+ }
+
+ Debug.Assert(Options.SurrogateSelector is not null);
+
+ if (!_surrogates.TryGetValue(type, out ISerializationSurrogate? surrogate))
+ {
+ surrogate = Options.SurrogateSelector.GetSurrogate(type, Options.StreamingContext, out _);
+ _surrogates[type] = surrogate;
+ }
+
+ return surrogate;
+ }
+
+ void IDeserializer.PendSerializationInfo(PendingSerializationInfo pending)
+ {
+ _pendingSerializationInfo ??= new();
+ _pendingSerializationInfo.Enqueue(pending);
+ _pendingSerializationInfoIds ??= [];
+ _pendingSerializationInfoIds.Add(pending.ObjectId);
+ }
+
+ void IDeserializer.PendValueUpdater(ValueUpdater updater)
+ {
+ // Add the pending update and update the dependencies list.
+
+ _pendingUpdates ??= [];
+ _pendingUpdates.Add(updater);
+
+ _incompleteDependencies ??= [];
+
+ if (_incompleteDependencies.TryGetValue(updater.ObjectId, out HashSet? dependencies))
+ {
+ dependencies.Add(updater.ValueId);
+ }
+ else
+ {
+ _incompleteDependencies.Add(updater.ObjectId, [updater.ValueId]);
+ }
+ }
+
+ [UnconditionalSuppressMessage(
+ "Trimming",
+ "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code",
+ Justification = "The type is already in the cache of the TypeResolver, no need to mark this one again.")]
+ void IDeserializer.CompleteObject(Id id)
+ {
+ // Need to use a queue as Completion is recursive.
+
+ _pendingCompletions.Enqueue(id);
+ Id completed = Id.Null;
+
+ while (_pendingCompletions.Count > 0)
+ {
+ int completedId = _pendingCompletions.Dequeue();
+ _incompleteObjects.Remove(completedId);
+
+ // When we've recursed, we've done so because there are no more dependencies for the current id, so we can
+ // remove it from the dictionary. We have to pend as we can't remove while we're iterating the dictionary.
+ if (!completed.IsNull)
+ {
+ _incompleteDependencies?.Remove(completed);
+
+ if (_pendingSerializationInfoIds is not null && _pendingSerializationInfoIds.Contains(completed))
+ {
+ // We came back for an object that has no remaining direct dependencies, but still has
+ // PendingSerializationInfo. As such it cannot be considered completed yet.
+ continue;
+ }
+
+ completed = Id.Null;
+ }
+
+ if (_recordMap[completedId] is ClassRecord classRecord
+ && (_incompleteDependencies is null || !_incompleteDependencies.ContainsKey(completedId)))
+ {
+ // There are no remaining dependencies. Hook any finished events for this object.
+ // Doing at the end of deserialization for simplicity.
+
+ Type type = _typeResolver.GetType(classRecord.TypeName);
+ object @object = _deserializedObjects[completedId];
+
+ OnDeserialized += SerializationEvents.GetOnDeserializedForType(type, @object);
+
+ if (@object is IDeserializationCallback callback)
+ {
+ OnDeserialization += callback.OnDeserialization;
+ }
+
+ if (@object is IObjectReference objectReference)
+ {
+ _deserializedObjects[completedId] = objectReference.GetRealObject(Options.StreamingContext);
+ }
+ }
+
+ if (_incompleteDependencies is null)
+ {
+ continue;
+ }
+
+ Debug.Assert(_pendingUpdates is not null);
+
+ foreach (KeyValuePair> pair in _incompleteDependencies)
+ {
+ int incompleteId = pair.Key;
+ HashSet dependencies = pair.Value;
+
+ if (!dependencies.Remove(completedId))
+ {
+ continue;
+ }
+
+ // Search for fixups that need to be applied for this dependency.
+ int removals = _pendingUpdates.RemoveWhere((ValueUpdater updater) =>
+ {
+ if (updater.ValueId != completedId)
+ {
+ return false;
+ }
+
+ updater.UpdateValue(_deserializedObjects);
+ return true;
+ });
+
+ if (dependencies.Count != 0)
+ {
+ continue;
+ }
+
+ // No more dependencies, enqueue for completion
+ completed = incompleteId;
+ _pendingCompletions.Enqueue(incompleteId);
+ }
+ }
+ }
+}
+
+#pragma warning restore SYSLIB0050 // Type or member is obsolete
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/FieldValueUpdater.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/FieldValueUpdater.cs
new file mode 100644
index 0000000000000..646cbed28345c
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/FieldValueUpdater.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace System.Resources.Extensions.BinaryFormat.Deserializer;
+
+internal sealed class FieldValueUpdater : ValueUpdater
+{
+ private readonly FieldInfo _field;
+
+ internal FieldValueUpdater(int objectId, int valueId, FieldInfo field) : base(objectId, valueId)
+ {
+ _field = field;
+ }
+
+ internal override void UpdateValue(IDictionary objects)
+ {
+ object newValue = objects[ValueId];
+ _field.SetValue(objects[ObjectId], newValue);
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/IDeserializer.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/IDeserializer.cs
new file mode 100644
index 0000000000000..166e17d192784
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/IDeserializer.cs
@@ -0,0 +1,73 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.BinaryFormat.Deserializer;
+
+#pragma warning disable SYSLIB0050 // Type or member is obsolete
+
+///
+/// Interface for deserialization used to define the coupling between the main
+/// and its s.
+///
+internal interface IDeserializer
+{
+ ///
+ /// The current deserialization options.
+ ///
+ BinaryFormattedObject.Options Options { get; }
+
+ ///
+ /// The set of object record ids that are not considered "complete" yet.
+ ///
+ ///
+ ///
+ /// Objects are considered incomplete if they contain references to value or types
+ /// that need completed or if they have not yet finished evaluating all of their member data. They are also
+ /// considered incomplete if they implement or have a surrogate and the
+ /// has not yet been applied.
+ ///
+ ///
+ HashSet IncompleteObjects { get; }
+
+ ///
+ /// The set of objects that have been deserialized, indexed by record id.
+ ///
+ ///
+ ///
+ /// Objects may not be fully filled out. If they are not in , they are
+ /// guaranteed to have their reference type members created (this is not transitive- their members may not
+ /// be ready if there are cycles in the object graph).
+ ///
+ ///
+ IDictionary DeserializedObjects { get; }
+
+ ///
+ /// Resolver for types.
+ ///
+ BinaryFormattedObject.ITypeResolver TypeResolver { get; }
+
+ ///
+ /// Pend the given value updater to be run when it's value type dependency is complete.
+ ///
+ void PendValueUpdater(ValueUpdater updater);
+
+ ///
+ /// Pend a to be applied when the graph is fully parsed.
+ ///
+ void PendSerializationInfo(PendingSerializationInfo pending);
+
+ ///
+ /// Mark the object id as complete. This will check dependencies and resolve relevant s.
+ ///
+ void CompleteObject(Id id);
+
+ ///
+ /// Check for a surrogate for the given type. If none exists, returns .
+ ///
+ ISerializationSurrogate? GetSurrogate(Type type);
+}
+
+#pragma warning restore SYSLIB0050 // Type or member is obsolete
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ObjectRecordDeserializer.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ObjectRecordDeserializer.cs
new file mode 100644
index 0000000000000..9351e1718b635
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ObjectRecordDeserializer.cs
@@ -0,0 +1,103 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.BinaryFormat;
+
+namespace System.Resources.Extensions.BinaryFormat.Deserializer;
+
+#pragma warning disable SYSLIB0050 // Type or member is obsolete
+
+internal abstract partial class ObjectRecordDeserializer
+{
+ // Used to indicate that the value is missing from the deserialized objects.
+ private protected static object s_missingValueSentinel = new();
+
+ internal SerializationRecord ObjectRecord { get; }
+
+ [AllowNull]
+ internal object Object { get; private protected set; }
+
+ private protected IDeserializer Deserializer { get; }
+
+ private protected ObjectRecordDeserializer(SerializationRecord objectRecord, IDeserializer deserializer)
+ {
+ Deserializer = deserializer;
+ ObjectRecord = objectRecord;
+ }
+
+ ///
+ /// Continue parsing.
+ ///
+ /// The id that is necessary to complete parsing or if complete.
+ internal abstract Id Continue();
+
+ ///
+ /// Gets the actual object for a member value primitive or record. Returns if
+ /// the object record has not been encountered yet.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private protected (object? value, Id id) UnwrapMemberValue(object? memberValue)
+ {
+ if (memberValue is null) // PayloadReader does not return NullRecord, just null
+ {
+ return (null, Id.Null);
+ }
+ else if (memberValue is not SerializationRecord serializationRecord) // a primitive value
+ {
+ return (memberValue, Id.Null);
+ }
+ else if (serializationRecord.RecordType is RecordType.BinaryObjectString)
+ {
+ PrimitiveTypeRecord stringRecord = (PrimitiveTypeRecord)serializationRecord;
+ return (stringRecord.Value, stringRecord.ObjectId);
+ }
+ else if (serializationRecord.RecordType is RecordType.MemberPrimitiveTyped)
+ {
+ return (serializationRecord.GetMemberPrimitiveTypedValue(), Id.Null);
+ }
+ else
+ {
+ // ClassRecords & ArrayRecords
+ return TryGetObject(serializationRecord.ObjectId);
+ }
+
+ (object? value, Id id) TryGetObject(Id id)
+ {
+ if (!Deserializer.DeserializedObjects.TryGetValue(id, out object? value))
+ {
+ return (s_missingValueSentinel, id);
+ }
+
+ ValidateNewMemberObjectValue(value);
+ return (value, id);
+ }
+ }
+
+ ///
+ /// Called for new non-primitive reference types.
+ ///
+ private protected virtual void ValidateNewMemberObjectValue(object value) { }
+
+ ///
+ /// Returns if the given record's value needs an updater applied.
+ ///
+ private protected bool DoesValueNeedUpdated(object value, Id valueRecord) =>
+ // Null Id is a primitive value.
+ !valueRecord.IsNull
+ // IObjectReference is going to have its object replaced.
+ && (value is IObjectReference
+ // Value types that aren't "complete" need to be reapplied.
+ || (Deserializer.IncompleteObjects.Contains(valueRecord) && value.GetType().IsValueType));
+
+ [RequiresUnreferencedCode("Calls System.Windows.Forms.BinaryFormat.Deserializer.ClassRecordParser.Create(ClassRecord, IDeserializer)")]
+ internal static ObjectRecordDeserializer Create(SerializationRecord record, IDeserializer deserializer) => record switch
+ {
+ ClassRecord classRecord => ClassRecordDeserializer.Create(classRecord, deserializer),
+ _ => new ArrayRecordDeserializer((ArrayRecord)record, deserializer),
+ };
+}
+
+#pragma warning restore SYSLIB0050 // Type or member is obsolete
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/PendingSerializationInfo.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/PendingSerializationInfo.cs
new file mode 100644
index 0000000000000..4031835e2ca81
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/PendingSerializationInfo.cs
@@ -0,0 +1,83 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.BinaryFormat.Deserializer;
+
+#pragma warning disable SYSLIB0050 // Type or member is obsolete
+
+internal sealed class PendingSerializationInfo
+{
+ internal int ObjectId { get; }
+ private readonly ISerializationSurrogate? _surrogate;
+ private readonly SerializationInfo _info;
+
+ internal PendingSerializationInfo(
+ int objectId,
+ SerializationInfo info,
+ ISerializationSurrogate? surrogate)
+ {
+ ObjectId = objectId;
+ _surrogate = surrogate;
+ _info = info;
+ }
+
+ [RequiresUnreferencedCode("We can't guarantee that the ctor will be present, as the type is not known up-front.")]
+ internal void Populate(IDictionary objects, StreamingContext context)
+ {
+ object @object = objects[ObjectId];
+ Type type = @object.GetType();
+
+ if (_surrogate is not null)
+ {
+ object populated = _surrogate.SetObjectData(@object, _info, context, selector: null);
+ if (populated is null)
+ {
+ // Sort of odd to allow returning null to ignore setting the returned value back,
+ // but that is the way this worked in the BinaryFormatter.
+ return;
+ }
+
+ // Don't use == on reference types as we are dependent on the instance in the objects
+ // dictionary never changing. Value types can be modified but this is ok as any usages
+ // of this object will have a pending fixup that will reapply the value.
+ //
+ // BinaryFormatter would allow this to change as long as you didn't wrap the surrogate
+ // in FormaterServices.GetSurrogateForCyclicalReference. We break here on value types
+ // as they can never be observed in an unfinished state.
+ if (!type.IsValueType && !ReferenceEquals(populated, @object))
+ {
+ throw new SerializationException(SR.Serialization_Surrogates);
+ }
+
+ objects[ObjectId] = populated;
+ return;
+ }
+
+ ConstructorInfo constructor = GetDeserializationConstructor(type);
+ constructor.Invoke(@object, [_info, context]);
+ }
+
+ private static ConstructorInfo GetDeserializationConstructor(
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.NonPublicConstructors)] Type type)
+ {
+ foreach (ConstructorInfo constructor in type.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly))
+ {
+ ParameterInfo[] parameters = constructor.GetParameters();
+ if (parameters.Length == 2
+ && parameters[0].ParameterType == typeof(SerializationInfo)
+ && parameters[1].ParameterType == typeof(StreamingContext))
+ {
+ return constructor;
+ }
+ }
+
+ throw new SerializationException(SR.Format(SR.Serialization_MissingCtor, type.FullName));
+ }
+}
+
+#pragma warning restore SYSLIB0050 // Type or member is obsolete
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/SerializationInfoValueUpdater.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/SerializationInfoValueUpdater.cs
new file mode 100644
index 0000000000000..fe76daf424491
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/SerializationInfoValueUpdater.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.BinaryFormat.Deserializer;
+
+internal sealed class SerializationInfoValueUpdater : ValueUpdater
+{
+ private readonly SerializationInfo _info;
+ private readonly string _name;
+
+ internal SerializationInfoValueUpdater(int objectId, int valueId, SerializationInfo info, string name) : base(objectId, valueId)
+ {
+ _info = info;
+ _name = name;
+ }
+
+ internal override void UpdateValue(IDictionary objects)
+ {
+ object newValue = objects[ValueId];
+ _info.UpdateValue(_name, newValue, newValue.GetType());
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ValueUpdater.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ValueUpdater.cs
new file mode 100644
index 0000000000000..973392c2020c3
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Deserializer/ValueUpdater.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+
+namespace System.Resources.Extensions.BinaryFormat.Deserializer;
+
+internal abstract class ValueUpdater
+{
+ ///
+ /// The value id that needs to be reapplied.
+ ///
+ internal int ValueId { get; }
+
+ ///
+ /// The object id that is dependent on .
+ ///
+ internal int ObjectId { get; }
+
+ private protected ValueUpdater(int objectId, int valueId)
+ {
+ ObjectId = objectId;
+ ValueId = valueId;
+ }
+
+ internal abstract void UpdateValue(IDictionary objects);
+}
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Id.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Id.cs
new file mode 100644
index 0000000000000..e64031f4646f0
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/Id.cs
@@ -0,0 +1,46 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+
+namespace System.Resources.Extensions;
+
+///
+/// Identifier struct.
+///
+internal readonly struct Id : IEquatable
+{
+ private readonly int _id;
+ private readonly bool _isNull = true;
+
+ // It is possible that the id may be negative with value types. See BinaryObjectWriter.InternalGetId.
+ private Id(int id)
+ {
+ _id = id;
+ _isNull = false;
+ }
+
+ private Id(bool isNull)
+ {
+ _id = 0;
+ _isNull = isNull;
+ }
+
+ public static Id Null => new(isNull: true);
+ public bool IsNull => _isNull;
+
+ public static implicit operator int(Id value) => value._isNull ? throw new InvalidOperationException() : value._id;
+ public static implicit operator Id(int value) => new(value);
+
+ public override bool Equals([NotNullWhen(true)] object? obj)
+ => (obj is Id id && Equals(id)) || (obj is int value && value == _id);
+
+ public bool Equals(Id other) => _isNull == other._isNull && _id == other._id;
+
+ public override readonly int GetHashCode() => _id.GetHashCode();
+ public override readonly string ToString() => _isNull ? "" : _id.ToString();
+
+ public static bool operator ==(Id left, Id right) => left.Equals(right);
+
+ public static bool operator !=(Id left, Id right) => !(left == right);
+}
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/SerializationEvents.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/SerializationEvents.cs
new file mode 100644
index 0000000000000..717873f236b0d
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/SerializationEvents.cs
@@ -0,0 +1,114 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.BinaryFormat;
+
+internal sealed class SerializationEvents
+{
+ private static readonly ConcurrentDictionary s_cache = new();
+
+ private static readonly SerializationEvents s_noEvents = new();
+
+ private readonly List? _onDeserializingMethods;
+ private readonly List? _onDeserializedMethods;
+
+ private SerializationEvents() { }
+
+ private SerializationEvents(
+ List? onDeserializingMethods,
+ List? onDeserializedMethods)
+ {
+ _onDeserializingMethods = onDeserializingMethods;
+ _onDeserializedMethods = onDeserializedMethods;
+ }
+
+ [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2111:UnrecognizedReflectionPattern",
+ Justification = "The Type is annotated correctly, it just can't pass through the lambda method.")]
+ private static SerializationEvents GetSerializationEventsForType(
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type t) =>
+ s_cache.GetOrAdd(t, CreateSerializationEvents);
+
+ private static SerializationEvents CreateSerializationEvents([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type)
+ {
+ List? onDeserializingMethods = GetMethodsWithAttribute(typeof(OnDeserializingAttribute), type);
+ List? onDeserializedMethods = GetMethodsWithAttribute(typeof(OnDeserializedAttribute), type);
+
+ return onDeserializingMethods is null && onDeserializedMethods is null
+ ? s_noEvents
+ : new SerializationEvents(onDeserializingMethods, onDeserializedMethods);
+ }
+
+ private static List? GetMethodsWithAttribute(
+ Type attribute,
+ // Currently the only way to preserve base, non-public methods is to use All
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type? type)
+ {
+ List? attributedMethods = null;
+
+ // Traverse the hierarchy to find all methods with the specified attribute.
+ Type? baseType = type;
+ while (baseType is not null && baseType != typeof(object))
+ {
+ MethodInfo[] methods = baseType.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
+ foreach (MethodInfo method in methods)
+ {
+ if (method.IsDefined(attribute, inherit: false))
+ {
+ attributedMethods ??= [];
+ attributedMethods.Add(method);
+ }
+ }
+
+ baseType = baseType.BaseType;
+ }
+
+ // We should invoke the methods starting from base.
+ attributedMethods?.Reverse();
+
+ return attributedMethods;
+ }
+
+ internal static Action? GetOnDeserializingForType(
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type,
+ object obj) =>
+ GetSerializationEventsForType(type).GetOnDeserializing(obj);
+
+ internal static Action? GetOnDeserializedForType(
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type type,
+ object obj) =>
+ GetSerializationEventsForType(type).GetOnDeserialized(obj);
+
+ private Action? GetOnDeserialized(object obj) =>
+ AddOnDelegate(obj, _onDeserializedMethods);
+
+ private Action? GetOnDeserializing(object obj) =>
+ AddOnDelegate(obj, _onDeserializingMethods);
+
+ /// Add all methods to a delegate.
+ private static Action? AddOnDelegate(object obj, List? methods)
+ {
+ Action? handler = null;
+
+ if (methods is not null)
+ {
+ foreach (MethodInfo method in methods)
+ {
+ Action onDeserialized =
+#if NETCOREAPP
+ method.CreateDelegate>(obj);
+#else
+ (Action)method.CreateDelegate(typeof(Action), obj);
+#endif
+ handler += onDeserialized;
+ }
+ }
+
+ return handler;
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/SerializationExtensions.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/SerializationExtensions.cs
new file mode 100644
index 0000000000000..f5c1d09ff9fa0
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/SerializationExtensions.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.ExceptionServices;
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.BinaryFormat;
+
+internal static class SerializationExtensions
+{
+ ///
+ /// Converts the given exception to a if needed, nesting the original exception
+ /// and assigning the original stack trace.
+ ///
+ public static SerializationException ConvertToSerializationException(this Exception ex)
+ => ex is SerializationException serializationException
+ ? serializationException
+#if NETCOREAPP
+ : (SerializationException)ExceptionDispatchInfo.SetRemoteStackTrace(
+ new SerializationException(ex.Message, ex),
+ ex.StackTrace ?? string.Empty);
+#else
+ : new SerializationException(ex.Message, ex);
+#endif
+}
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/SerializationInfoExtensions.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/SerializationInfoExtensions.cs
new file mode 100644
index 0000000000000..3cdf78d495322
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/BinaryFormat/SerializationInfoExtensions.cs
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Diagnostics.CodeAnalysis;
+using System.Reflection;
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.BinaryFormat;
+
+internal static class SerializationInfoExtensions
+{
+ private static readonly Action s_updateValue =
+ typeof(SerializationInfo)
+ .GetMethod("UpdateValue", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)!
+#if NETCOREAPP
+ .CreateDelegate>();
+#else
+ .CreateDelegate(typeof(Action)) as Action;
+#endif
+
+
+ [DynamicDependency(DynamicallyAccessedMemberTypes.NonPublicMethods, typeof(SerializationInfo))]
+ internal static void UpdateValue(this SerializationInfo si, string name, object value, Type type) =>
+ s_updateValue(si, name, value, type);
+}
diff --git a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/DeserializingResourceReader.cs b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/DeserializingResourceReader.cs
index 2d626423506df..6cb1dfe45de62 100644
--- a/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/DeserializingResourceReader.cs
+++ b/src/libraries/System.Resources.Extensions/src/System/Resources/Extensions/DeserializingResourceReader.cs
@@ -3,6 +3,7 @@
using System.ComponentModel;
using System.IO;
+using System.Resources.Extensions.BinaryFormat;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
@@ -10,12 +11,9 @@ namespace System.Resources.Extensions
{
public partial class DeserializingResourceReader
{
- private bool _assumeBinaryFormatter;
+ private static readonly bool s_useBinaryFormatter = AppContext.TryGetSwitch("System.Resources.Extensions.UseBinaryFormatter", out bool isEnabled) && isEnabled;
-// Issue https://github.com/dotnet/runtime/issues/39292 tracks finding an alternative to BinaryFormatter
-#pragma warning disable SYSLIB0011
- private BinaryFormatter? _formatter;
-#pragma warning restore SYSLIB0011
+ private bool _assumeBinaryFormatter;
private bool ValidateReaderType(string readerType)
{
@@ -37,18 +35,25 @@ private bool ValidateReaderType(string readerType)
return false;
}
-// Issue https://github.com/dotnet/runtime/issues/39292 tracks finding an alternative to BinaryFormatter
-#pragma warning disable SYSLIB0011
private object ReadBinaryFormattedObject()
{
- _formatter ??= new BinaryFormatter()
+ if (!s_useBinaryFormatter)
{
- Binder = new UndoTruncatedTypeNameSerializationBinder()
- };
+ BinaryFormattedObject binaryFormattedObject = new(_store.BaseStream);
- return _formatter.Deserialize(_store.BaseStream);
- }
+ return binaryFormattedObject.Deserialize();
+ }
+ else
+ {
+#pragma warning disable SYSLIB0011
+ BinaryFormatter? formatter = new()
+ {
+ Binder = new UndoTruncatedTypeNameSerializationBinder()
+ };
+ return formatter.Deserialize(_store.BaseStream);
#pragma warning restore SYSLIB0011
+ }
+ }
internal sealed class UndoTruncatedTypeNameSerializationBinder : SerializationBinder
{
@@ -227,6 +232,5 @@ private object DeserializeObject(int typeIndex)
return value;
}
-
}
}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/ArrayTests.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/ArrayTests.cs
new file mode 100644
index 0000000000000..388095dd55858
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/ArrayTests.cs
@@ -0,0 +1,38 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Resources.Extensions.Tests.Common;
+
+public abstract class ArrayTests : SerializationTest where T : ISerializer
+{
+ [Fact]
+ public virtual void Roundtrip_ArrayContainingArrayAtNonZeroLowerBound()
+ {
+ // Not supported by BinaryFormattedObject
+ RoundTrip(Array.CreateInstance(typeof(uint[]), [5], [1]));
+ }
+
+ [Fact]
+ public void ArraySerializableValueType()
+ {
+ nint[] nints = [42, 43, 44];
+ object deserialized = Deserialize(Serialize(nints));
+ deserialized.Should().BeEquivalentTo(nints);
+ }
+
+ [Fact]
+ public void SameObjectRepeatedInArray()
+ {
+ object o = new();
+ object[] arr = [o, o, o, o, o];
+ object[] result = (object[])Deserialize(Serialize(arr));
+
+ Assert.Equal(arr.Length, result.Length);
+ Assert.NotSame(arr, result);
+ Assert.NotSame(arr[0], result[0]);
+ for (int i = 1; i < result.Length; i++)
+ {
+ Assert.Same(result[0], result[i]);
+ }
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/BasicObjectTests.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/BasicObjectTests.cs
new file mode 100644
index 0000000000000..db240fc90e0e4
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/BasicObjectTests.cs
@@ -0,0 +1,85 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Linq;
+using System.Runtime.Serialization.Formatters;
+using BinaryFormatTests;
+using BinaryFormatTests.FormatterTests;
+
+namespace System.Resources.Extensions.Tests.Common;
+
+public abstract class BasicObjectTests : SerializationTest where T : ISerializer
+{
+ private protected abstract bool SkipOffsetArrays { get; }
+
+ [Theory]
+ [MemberData(nameof(SerializableObjects))]
+ public void DeserializeStoredObjects(object value, TypeSerializableValue[] serializedData)
+ {
+ _ = value;
+
+ int platformIndex = serializedData.GetPlatformIndex();
+ for (int i = 0; i < serializedData.Length; i++)
+ {
+ for (FormatterAssemblyStyle assemblyMatching = 0; assemblyMatching <= FormatterAssemblyStyle.Full; assemblyMatching++)
+ {
+ object deserialized = DeserializeFromBase64Chars(serializedData[i].Base64Blob, assemblyMatching: assemblyMatching);
+
+ if (deserialized is StringComparer)
+ {
+ // StringComparer derived classes are not public and they don't serialize the actual type.
+ value.Should().BeAssignableTo();
+ }
+ else
+ {
+ deserialized.Should().BeOfType(value.GetType());
+ }
+
+ bool isSamePlatform = i == platformIndex;
+ EqualityExtensions.CheckEquals(value, deserialized, isSamePlatform);
+ }
+ }
+ }
+
+ [Theory]
+ [MemberData(nameof(BasicObjectsRoundtrip_MemberData))]
+ public void BasicObjectsRoundtrip(
+ object value,
+ FormatterAssemblyStyle assemblyMatching,
+ FormatterTypeStyle typeStyle)
+ {
+ object deserialized = RoundTrip(value, typeStyle: typeStyle, assemblyMatching: assemblyMatching);
+
+ // string.Empty and DBNull are both singletons
+ if (!ReferenceEquals(value, string.Empty)
+ && value is not DBNull
+ && value is Array array
+ && array.Length > 0)
+ {
+ deserialized.Should().NotBeSameAs(value);
+ }
+
+ EqualityExtensions.CheckEquals(value, deserialized, isSamePlatform: true);
+ }
+
+ public static EnumerableTupleTheoryData SerializableObjects()
+ {
+ // Can add a .Skip() to get to the failing scenario easier when debugging.
+ return new EnumerableTupleTheoryData((
+ // Explicitly not supporting offset arrays
+ from value in BinaryFormatterTests.RawSerializableObjects()
+ where value.Item1 is not Array array || array.GetLowerBound(0) == 0
+ select value).ToArray());
+ }
+
+ public static EnumerableTupleTheoryData BasicObjectsRoundtrip_MemberData()
+ {
+ return new EnumerableTupleTheoryData((
+ // Explicitly not supporting offset arrays
+ from value in BinaryFormatterTests.RawSerializableObjects()
+ from FormatterAssemblyStyle assemblyFormat in new[] { FormatterAssemblyStyle.Full, FormatterAssemblyStyle.Simple }
+ from FormatterTypeStyle typeFormat in new[] { FormatterTypeStyle.TypesAlways, FormatterTypeStyle.TypesAlways | FormatterTypeStyle.XsdString }
+ where value.Item1 is not Array array || array.GetLowerBound(0) == 0
+ select (value.Item1, assemblyFormat, typeFormat)).ToArray());
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/ComparerTests.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/ComparerTests.cs
new file mode 100644
index 0000000000000..8dfcce08130c7
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/ComparerTests.cs
@@ -0,0 +1,33 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using static BinaryFormatTests.FormatterTests.BinaryFormatterTests;
+
+namespace System.Resources.Extensions.Tests.Common;
+
+public abstract class ComparerTests : SerializationTest where T : ISerializer
+{
+ public static TheoryData NullableComparersTestData => new()
+ {
+ { "NullableEqualityComparer`1", EqualityComparer.Default },
+ { "NullableEqualityComparer`1", EqualityComparer.Default },
+ { "NullableEqualityComparer`1", EqualityComparer.Default },
+ { "NullableEqualityComparer`1", EqualityComparer.Default }, // implements IEquatable<>
+ { "ObjectEqualityComparer`1", EqualityComparer.Default }, // doesn't implement IEquatable<>
+ { "ObjectEqualityComparer`1", EqualityComparer.Default },
+ { "NullableComparer`1", Comparer.Default },
+ { "NullableComparer`1", Comparer.Default },
+ { "NullableComparer`1", Comparer.Default },
+ { "NullableComparer`1", Comparer.Default },
+ { "ObjectComparer`1", Comparer.Default },
+ { "ObjectComparer`1", Comparer.Default }
+ };
+
+ [Theory]
+ [MemberData(nameof(NullableComparersTestData))]
+ public void NullableComparers_Roundtrip(string expectedType, object obj)
+ {
+ object roundTrip = RoundTrip(obj);
+ roundTrip.GetType().Name.Should().Be(expectedType);
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/CorruptedTests.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/CorruptedTests.cs
new file mode 100644
index 0000000000000..9ea3839e37472
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/CorruptedTests.cs
@@ -0,0 +1,127 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Resources.Extensions.Tests.Common.TestTypes;
+using System.Resources.Extensions.Tests.FormattedObject;
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.BinaryFormat;
+using System.Text;
+
+namespace System.Resources.Extensions.Tests.Common;
+
+public class CorruptedTests : SerializationTest
+{
+ private const int ClassId = 1, LibraryId = 2;
+
+ [Fact]
+ public void ValueTypeReferencesSelf()
+ {
+ using MemoryStream stream = new();
+ BinaryWriter writer = new(stream, Encoding.UTF8);
+
+ WriteSerializedStreamHeader(writer);
+ WriteBinaryLibrary(writer, LibraryId, typeof(NodeStruct).Assembly.FullName!);
+ WriteClassInfo(writer, ClassId, typeof(NodeStruct).FullName!, ["Node"]);
+ WriteClassFieldInfo(writer, typeof(NodeWithNodeStruct).FullName!, LibraryId);
+ writer.Write(LibraryId);
+ WriteMemberReference(writer, ClassId);
+ WriteMessageEnd(writer);
+
+ stream.Position = 0;
+
+ // This fails in the SerializationConstructor in BinaryFormattedObject's deserializer because the
+ // type isn't convertible to NodeWithNodeStruct. In BinaryFormatter it fails with fixups.
+ Action action = () => Deserialize(stream);
+ action.Should().Throw();
+ }
+
+ [Fact]
+ public void ValueTypeReferencesSelf2()
+ {
+ using MemoryStream stream = new();
+ BinaryWriter writer = new(stream, Encoding.UTF8);
+
+ WriteSerializedStreamHeader(writer);
+ WriteBinaryLibrary(writer, LibraryId, typeof(StructWithObject).Assembly.FullName!);
+ WriteClassInfo(writer, ClassId, typeof(StructWithObject).FullName!, ["Value"]);
+ WriteClassFieldInfo(writer, typeof(StructWithObject).FullName!, LibraryId);
+ writer.Write(LibraryId);
+ WriteMemberReference(writer, ClassId);
+ WriteMessageEnd(writer);
+
+ stream.Position = 0;
+
+ Action action = () => Deserialize(stream);
+ action.Should().Throw();
+ }
+
+ [Fact]
+ public void ValueTypeReferencesSelf3()
+ {
+ using MemoryStream stream = new();
+ BinaryWriter writer = new(stream, Encoding.UTF8);
+
+ WriteSerializedStreamHeader(writer);
+ WriteBinaryLibrary(writer, LibraryId, typeof(StructWithTwoObjects).Assembly.FullName!);
+ WriteClassInfo(writer, ClassId, typeof(StructWithTwoObjects).FullName!, ["Value", "Value2"]);
+ writer.Write((byte)BinaryType.Object);
+ writer.Write((byte)BinaryType.Object);
+ writer.Write(LibraryId);
+ WriteMemberReference(writer, ClassId);
+ WriteMemberReference(writer, ClassId);
+ WriteMessageEnd(writer);
+
+ // Both deserializers create this where every boxed struct is the exact same boxed instance.
+ stream.Position = 0;
+
+ Action action = () => Deserialize(stream);
+ action.Should().Throw();
+ }
+
+ [Fact]
+ public virtual void ValueTypeReferencesSelf4()
+ {
+ using MemoryStream stream = new();
+ BinaryWriter writer = new(stream, Encoding.UTF8);
+
+ WriteSerializedStreamHeader(writer);
+ WriteBinaryLibrary(writer, LibraryId, typeof(StructWithTwoObjectsISerializable).Assembly.FullName!);
+ WriteClassInfo(writer, ClassId, typeof(StructWithTwoObjectsISerializable).FullName!, ["Value", "Value2"]);
+ writer.Write((byte)BinaryType.Object);
+ writer.Write((byte)BinaryType.Object);
+ writer.Write(LibraryId);
+ WriteMemberReference(writer, ClassId);
+ WriteMemberReference(writer, ClassId);
+ WriteMessageEnd(writer);
+
+ stream.Position = 0;
+ Deserialize(stream);
+ }
+
+ [Fact]
+ public virtual void ValueTypeReferencesSelf5()
+ {
+ const int NextClassId = 3;
+ using MemoryStream stream = new();
+ BinaryWriter writer = new(stream, Encoding.UTF8);
+
+ WriteSerializedStreamHeader(writer);
+ WriteBinaryLibrary(writer, LibraryId, typeof(StructWithTwoObjectsISerializable).Assembly.FullName!);
+ WriteClassInfo(writer, ClassId, typeof(StructWithTwoObjectsISerializable).FullName!, ["Value", "Value2"]);
+ writer.Write((byte)BinaryType.Object);
+ writer.Write((byte)BinaryType.Object);
+ writer.Write(LibraryId);
+ WriteMemberReference(writer, NextClassId);
+ WriteMemberReference(writer, NextClassId);
+ // ClassWithId
+ writer.Write((byte)RecordType.ClassWithId);
+ writer.Write(NextClassId); // id
+ writer.Write(ClassId); // id of the class that provides metadata
+ WriteMemberReference(writer, ClassId);
+ WriteMemberReference(writer, NextClassId);
+ WriteMessageEnd(writer);
+
+ stream.Position = 0;
+ Deserialize(stream);
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/CycleTests.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/CycleTests.cs
new file mode 100644
index 0000000000000..34b37f4b9573f
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/CycleTests.cs
@@ -0,0 +1,142 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Resources.Extensions.Tests.Common.TestTypes;
+
+namespace System.Resources.Extensions.Tests.Common;
+
+public abstract class CycleTests : SerializationTest where T : ISerializer
+{
+ [Fact]
+ public void SelfReferencingISerializableObject()
+ {
+ NodeWithValueISerializable node = new()
+ {
+ Value = 42
+ };
+
+ node.Node = node;
+
+ Stream stream = Serialize(node);
+
+ var deserialized = (NodeWithValueISerializable)Deserialize(stream);
+
+ deserialized.Value.Should().Be(42);
+ deserialized.Node.Should().BeSameAs(deserialized);
+ }
+
+ [Fact]
+ public void SimpleLoopingISerializableObjects()
+ {
+ NodeWithValueISerializable node1 = new()
+ {
+ Value = 42
+ };
+
+ NodeWithValueISerializable node2 = new()
+ {
+ Value = 43
+ };
+
+ node1.Node = node2;
+ node2.Node = node1;
+
+ Stream stream = Serialize(node1);
+
+ var deserialized = (NodeWithValueISerializable)Deserialize(stream);
+ deserialized.Value.Should().Be(42);
+ deserialized.Node!.Value.Should().Be(43);
+ deserialized.Node.Node.Should().BeSameAs(deserialized);
+ }
+
+ [Fact]
+ public virtual void BackPointerToISerializableClass()
+ {
+ ClassWithValueISerializable> @object = new();
+ StructWithReferenceISerializable structValue = new() { Value = 42, Reference = @object };
+ @object.Value = structValue;
+
+ Stream stream = Serialize(@object);
+
+ // BinaryFormatter doesn't handle this round trip.
+ var deserialized = (ClassWithValueISerializable>)Deserialize(stream);
+ deserialized.Value.Value.Should().Be(42);
+ deserialized.Value.Reference.Should().BeSameAs(deserialized);
+ }
+
+ [Fact]
+ public void BackPointerToArray()
+ {
+ var nints = new StructWithSelfArrayReferenceISerializable[3];
+ nints[0] = new() { Value = 42, Array = nints };
+ nints[1] = new() { Value = 43, Array = nints };
+ nints[2] = new() { Value = 44, Array = nints };
+
+ Stream stream = Serialize(nints);
+ var deserialized = (StructWithSelfArrayReferenceISerializable[])Deserialize(stream);
+
+ deserialized[0].Value.Should().Be(42);
+ deserialized[1].Value.Should().Be(43);
+ deserialized[2].Value.Should().Be(44);
+ deserialized[0].Array.Should().BeSameAs(deserialized);
+ }
+
+ [Fact]
+ public void BackPointerFromNestedStruct()
+ {
+ NodeWithNodeStruct node = new() { Value = "Root" };
+ node.NodeStruct = new NodeStruct { Node = node };
+
+ NodeWithNodeStruct deserialized = (NodeWithNodeStruct)Deserialize(Serialize(node));
+
+ deserialized.NodeStruct.Node.Should().BeSameAs(deserialized);
+ deserialized.Value.Should().Be("Root");
+ }
+
+ [Fact]
+ public void IndirectBackPointerFromNestedStruct()
+ {
+ NodeWithNodeStruct node = new() { Value = "Root" };
+ NodeWithNodeStruct node2 = new() { Value = "Node2" };
+ node.NodeStruct = new() { Node = node2 };
+ node2.NodeStruct = new() { Node = node };
+
+ NodeWithNodeStruct deserialized = (NodeWithNodeStruct)Deserialize(Serialize(node));
+
+ deserialized.Value.Should().Be("Root");
+ deserialized.NodeStruct.Node!.NodeStruct.Node.Should().BeSameAs(deserialized);
+ deserialized.NodeStruct.Node!.Value.Should().Be("Node2");
+ }
+
+ [Fact]
+ public void BinaryTreeCycles()
+ {
+ BinaryTreeNode root = new();
+ root.Left = root;
+
+ BinaryTreeNode deserialized = (BinaryTreeNode)Deserialize(Serialize(root));
+ deserialized.Left.Should().BeSameAs(deserialized);
+ deserialized.Right.Should().BeNull();
+
+ root.Right = root.Left;
+ deserialized = (BinaryTreeNode)Deserialize(Serialize(root));
+ deserialized.Left.Should().BeSameAs(deserialized);
+ deserialized.Right.Should().BeSameAs(deserialized);
+ }
+
+ [Fact]
+ public void BinaryTreeCycles_ISerializable()
+ {
+ BinaryTreeNodeISerializable root = new();
+ root.Left = root;
+
+ var deserialized = (BinaryTreeNodeISerializable)Deserialize(Serialize(root));
+ deserialized.Left.Should().BeSameAs(deserialized);
+ deserialized.Right.Should().BeNull();
+
+ root.Right = root.Left;
+ deserialized = (BinaryTreeNodeISerializable)Deserialize(Serialize(root));
+ deserialized.Left.Should().BeSameAs(deserialized);
+ deserialized.Right.Should().BeSameAs(deserialized);
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/DelegateBinder.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/DelegateBinder.cs
new file mode 100644
index 0000000000000..ef752193d98a3
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/DelegateBinder.cs
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.Tests.Common;
+
+internal class DelegateBinder : SerializationBinder
+{
+ public Func? BindToTypeDelegate;
+ public override Type? BindToType(string assemblyName, string typeName) => BindToTypeDelegate?.Invoke(assemblyName, typeName);
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/EventOrderTests.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/EventOrderTests.cs
new file mode 100644
index 0000000000000..4baf8d328f927
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/EventOrderTests.cs
@@ -0,0 +1,1022 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+using System.Resources.Extensions.Tests.Common.TestTypes;
+
+namespace System.Resources.Extensions.Tests.Common;
+
+[Collection("Sequential")]
+public abstract class EventOrderTests : SerializationTest where T : ISerializer
+{
+ #region Depth0
+ #region NoCycle
+ [Fact]
+ public void Depth0_NoCycle_ISerializable()
+ {
+ BinaryTreeNodeWithEventsISerializable root = new() { Name = "root" };
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal("roots", "rootp", "rooti");
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth0_NoCycle_ISerializable_WithValueType()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["p", "roots", "valuep", "valuei", "rooti"]
+ : ["roots", "valuep", "rootp", "valuei", "rooti"];
+ BinaryTreeNodeWithEventsISerializable root = new() { Name = "root", Value = new StructThatImplementsIDeserializationCallback() { Name = "value" } };
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth0_NoCycle_ISerializable_WithValueTypeISerializable()
+ {
+ BinaryTreeNodeWithEventsISerializable root = new() { Name = "root", Value = new ValueTypeISerializable() { Name = "value" } };
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal("values", "roots", "valuep", "rootp", "valuei", "rooti");
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth0_NoCycle()
+ {
+ BinaryTreeNodeWithEvents root = new() { Name = "root" };
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal("rootp", "rooti");
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth0_NoCycle_Surrogate()
+ {
+ SurrogateSelector selector = CreateSurrogateSelector(new BinaryTreeNodeWithEventsSurrogate());
+ BinaryTreeNodeWithEvents root = new() { Name = "root" };
+
+ try
+ {
+ Stream stream = Serialize(root);
+ Deserialize(stream, surrogateSelector: selector);
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal("roots", "rootp", "rooti");
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth0_NoCycle_WithValueType()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["rootp", "valuep", "valuei", "rooti"]
+ : ["valuep", "rootp", "valuei", "rooti"];
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Value = new StructThatImplementsIDeserializationCallback() { Name = "value" } };
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth0_NoCycle_WithValueTypeISerializable()
+ {
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Value = new ValueTypeISerializable() { Name = "value" } };
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal("values", "valuep", "rootp", "valuei", "rooti");
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth0_NoCycle_ValueTypeWithSurrogate()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["p", "roots", "valuep", "valuei", "rooti"]
+ : ["roots", "valuep", "rootp", "valuei", "rooti"];
+ SurrogateSelector selector = CreateSurrogateSelector(new BinaryTreeNodeWithEventsSurrogate());
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Value = new StructThatImplementsIDeserializationCallback() { Name = "value" } };
+
+ try
+ {
+ Stream stream = Serialize(root);
+ Deserialize(stream, surrogateSelector: selector);
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth0_NoCycle_ValueTypeISerializableWithSurrogate()
+ {;
+ SurrogateSelector selector = CreateSurrogateSelector(new BinaryTreeNodeWithEventsSurrogate());
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Value = new ValueTypeISerializable() { Name = "value" } };
+
+ try
+ {
+ Stream stream = Serialize(root);
+ Deserialize(stream, surrogateSelector: selector);
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal("values", "roots", "valuep", "rootp", "valuei", "rooti");
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ #endregion NoCycle
+
+ #region Cycle
+ [Fact]
+ public void Depth0_SelfCycle_ISerializable()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["p", "roots", "rooti"]
+ : ["roots", "rootp", "rooti"];
+ BinaryTreeNodeWithEventsISerializable root = new() { Name = "root" };
+ root.Left = root;
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth0_SelfCycle_ISerializable_WithValueType()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["p", "roots", "valuep", "valuei", "rooti"]
+ : ["roots", "valuep", "rootp", "valuei", "rooti"];
+ BinaryTreeNodeWithEventsISerializable root = new() { Name = "root", Value = new StructThatImplementsIDeserializationCallback() { Name = "value" } };
+ root.Left = root;
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth0_SelfCycle_ISerializable_WithValueTypeISerializable()
+ {
+ BinaryTreeNodeWithEventsISerializable root = new() { Name = "root", Value = new ValueTypeISerializable() { Name = "value" } };
+ root.Left = root;
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal("values", "roots", "valuep", "rootp", "valuei", "rooti");
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth0_SelfCycle()
+ {
+ BinaryTreeNodeWithEvents root = new() { Name = "root" };
+ root.Left = root;
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal("rootp", "rooti");
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth0_SelfCycle_Surrogate()
+ {
+ SurrogateSelector selector = CreateSurrogateSelector(new BinaryTreeNodeWithEventsSurrogate());
+ BinaryTreeNodeWithEvents root = new() { Name = "root" };
+ root.Left = root;
+
+ try
+ {
+ Stream stream = Serialize(root);
+ if (IsBinaryFormatterDeserializer)
+ {
+ Action action = () => Deserialize(stream, surrogateSelector: selector);
+ }
+ else
+ {
+ Deserialize(stream, surrogateSelector: selector);
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal("roots", "rootp", "rooti");
+ }
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth0_SelfCycle_WithValueType()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["rootp", "valuep", "valuei", "rooti"]
+ : ["valuep", "rootp", "valuei", "rooti"];
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Value = new StructThatImplementsIDeserializationCallback() { Name = "value" } };
+ root.Left = root;
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth0_SelfCycle_WithValueTypeISerializable()
+ {
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Value = new ValueTypeISerializable() { Name = "value" } };
+ root.Left = root;
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal("values", "valuep", "rootp", "valuei", "rooti");
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+ #endregion Cycle
+ #endregion Depth0
+
+ #region Depth1
+ #region NoCycle
+ [Fact]
+ public void Depth1_NoCycle_ISerializable()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["p", "childs", "roots", "rootp", "rooti", "childi"]
+ : ["childs", "roots", "childp", "rootp", "childi", "rooti"];
+ BinaryTreeNodeWithEventsISerializable child = new() { Name = "child" };
+ BinaryTreeNodeWithEventsISerializable root = new() { Name = "root", Left = child };
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth1_NoCycle_ISerializable_WithValueType()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["p", "childs", "roots", "valuep", "rootp", "valuei", "rooti", "childi"]
+ : ["childs", "roots", "valuep", "childp", "rootp", "valuei", "childi", "rooti"];
+ BinaryTreeNodeWithEventsISerializable child = new() { Name = "child" };
+ BinaryTreeNodeWithEventsISerializable root = new() { Name = "root", Left = child, Value = new StructThatImplementsIDeserializationCallback() { Name = "value" } };
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth1_NoCycle_ISerializable_WithValueTypeISerializable()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["p", "values", "childs", "roots", "valuep", "rootp", "valuei", "rooti", "childi"]
+ : ["childs", "values", "roots", "childp", "valuep", "rootp", "childi", "valuei", "rooti"];
+ BinaryTreeNodeWithEventsISerializable child = new() { Name = "child" };
+ BinaryTreeNodeWithEventsISerializable root = new() { Name = "root", Left = child, Value = new ValueTypeISerializable() { Name = "value" } };
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth1_NoCycle_ISerializable_WithValueTypeISerializable_WithReference()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["p", "childs", "values", "roots", "valuep", "rootp", "valuei", "rooti", "childi"]
+ : ["childs", "values", "roots", "childp", "valuep", "rootp", "childi", "valuei", "rooti"];
+ BinaryTreeNodeWithEventsISerializable child = new() { Name = "child" };
+ BinaryTreeNodeWithEventsISerializable root = new()
+ {
+ Name = "root",
+ Left = child,
+ Value = new ValueTypeISerializable() { Name = "value", Reference = child }
+ };
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth1_NoCycle()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["childp", "rootp", "rooti", "childi"]
+ : ["childp", "rootp", "childi", "rooti"];
+ BinaryTreeNodeWithEvents child = new() { Name = "child" };
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Left = child };
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth1_NoCycle_Surrogate()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["p", "childs", "roots", "rootp", "rooti", "childi"]
+ : ["childs", "roots", "childp", "rootp", "childi", "rooti"];
+ SurrogateSelector selector = CreateSurrogateSelector(new BinaryTreeNodeWithEventsSurrogate());
+ BinaryTreeNodeWithEvents child = new() { Name = "child" };
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Left = child };
+
+ try
+ {
+ Stream stream = Serialize(root);
+ Deserialize(stream, surrogateSelector: selector);
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth1_NoCycle_WithValueType()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["childp", "valuep", "rootp", "valuei", "rooti", "childi"]
+ : ["childp", "valuep", "rootp", "childi", "valuei", "rooti"];
+ BinaryTreeNodeWithEvents child = new() { Name = "child" };
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Left = child, Value = new StructThatImplementsIDeserializationCallback() { Name = "value" } };
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth1_NoCycle_WithValueTypeISerializable()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["childp", "values", "valuep", "rootp", "valuei", "rooti", "childi"]
+ : ["values", "childp", "valuep", "rootp", "childi", "valuei", "rooti" ];
+ BinaryTreeNodeWithEvents child = new() { Name = "child" };
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Left = child, Value = new ValueTypeISerializable() { Name = "value" } };
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+ #endregion NoCycle
+
+ #region Cycle
+ [Fact]
+ public void Depth1_Cycle_ISerializable()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["p", "childs", "roots", "rootp", "rooti", "childi"]
+ : ["childs", "roots", "childp", "rootp", "childi", "rooti"];
+ BinaryTreeNodeWithEventsISerializable child = new() { Name = "child" };
+ BinaryTreeNodeWithEventsISerializable root = new() { Name = "root", Left = child };
+ child.Left = root;
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth1_Cycle_ISerializable_WithValueType()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["p", "childs", "roots", "value2p", "rootp", "value1p", "value2i", "rooti", "value1i", "childi"]
+ : ["childs", "roots", "value1p", "value2p", "childp", "rootp", "value1i", "value2i", "childi", "rooti"];
+ BinaryTreeNodeWithEventsISerializable child = new() { Name = "child", Value = new StructThatImplementsIDeserializationCallback() { Name = "value1" } };
+ BinaryTreeNodeWithEventsISerializable root = new() { Name = "root", Left = child, Value = new StructThatImplementsIDeserializationCallback() { Name = "value2" } };
+ child.Left = root;
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth1_Cycle_ISerializable_WithValueTypeISerializable()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["value2s", "value1s", "childs", "roots", "value2p", "rootp", "value1p", "childp", "value2i", "rooti", "value1i", "childi"]
+ : ["value1s", "childs", "value2s", "roots", "value1p", "childp", "value2p", "rootp", "value1i", "childi", "value2i", "rooti"];
+ BinaryTreeNodeWithEventsISerializable child = new() { Name = "child", Value = new ValueTypeISerializable() { Name = "value1" } };
+ BinaryTreeNodeWithEventsISerializable root = new() { Name = "root", Left = child, Value = new ValueTypeISerializable() { Name = "value2" } };
+ child.Left = root;
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth1_Cycle()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["childp", "rootp", "rooti", "childi"]
+ : ["childp", "rootp", "childi", "rooti"];
+ BinaryTreeNodeWithEvents child = new() { Name = "child" };
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Left = child };
+ child.Left = root;
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth1_Cycle_Surrogate()
+ {
+ SurrogateSelector selector = CreateSurrogateSelector(new BinaryTreeNodeWithEventsSurrogate());
+ BinaryTreeNodeWithEvents child = new() { Name = "child" };
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Left = child };
+ child.Left = root;
+
+ try
+ {
+ Stream stream = Serialize(root);
+ if (IsBinaryFormatterDeserializer)
+ {
+ Action action = () => Deserialize(stream, surrogateSelector: selector);
+ action.Should().Throw();
+ }
+ else
+ {
+ Deserialize(stream, surrogateSelector: selector);
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal("childs", "roots", "childp", "rootp", "childi", "rooti");
+ }
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth1_Cycle_WithValueType()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["childp", "value2p", "rootp", "value1p", "value2i", "rooti", "value1i", "childi"]
+ : ["value1p", "childp", "value2p", "rootp", "value1i", "childi", "value2i", "rooti"];
+ BinaryTreeNodeWithEvents child = new() { Name = "child", Value = new StructThatImplementsIDeserializationCallback() { Name = "value1" } };
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Left = child, Value = new StructThatImplementsIDeserializationCallback() { Name = "value2" } };
+ child.Left = root;
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth1_Cycle_WithValueTypeISerializable()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["value2s", "value1s", "value2p", "rootp", "value1p", "childp", "value2i", "rooti", "value1i", "childi"]
+ : ["value1s", "value2s", "value1p", "childp", "value2p", "rootp", "value1i", "childi", "value2i", "rooti"];
+ BinaryTreeNodeWithEvents child = new() { Name = "child", Value = new ValueTypeISerializable() { Name = "value1" } };
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Left = child, Value = new ValueTypeISerializable() { Name = "value2" } };
+ child.Left = root;
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+ #endregion Cycle
+ #endregion Depth1
+
+ #region Depth2
+ #region NoCycle
+ [Fact]
+ public void Depth2_NoCycle_ISerializable()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["p", "child2s", "child1s", "roots", "rootp", "child1p", "rooti", "child1i", "child2i"]
+ : ["child2s", "child1s", "roots", "child2p", "child1p", "rootp", "child2i", "child1i", "rooti"];
+ BinaryTreeNodeWithEventsISerializable child2 = new() { Name = "child2" };
+ BinaryTreeNodeWithEventsISerializable child1 = new() { Name = "child1", Left = child2 };
+ BinaryTreeNodeWithEventsISerializable root = new() { Name = "root", Left = child1 };
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth2_NoCycle_ISerializable_WithValueType()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["p", "child2s", "child1s", "roots", "valuep", "rootp", "child1p", "valuei", "rooti", "child1i", "child2i"]
+ : ["child2s", "child1s", "roots", "valuep", "child2p", "child1p", "rootp", "valuei", "child2i", "child1i", "rooti"];
+ BinaryTreeNodeWithEventsISerializable child2 = new() { Name = "child2" };
+ BinaryTreeNodeWithEventsISerializable child1 = new() { Name = "child1", Left = child2 };
+ BinaryTreeNodeWithEventsISerializable root = new() { Name = "root", Left = child1, Value = new StructThatImplementsIDeserializationCallback() { Name = "value" } };
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth2_NoCycle_ISerializable_WithValueTypeISerializable()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["p", "values", "child2s", "child1s", "roots", "valuep", "rootp", "child1p", "valuei", "rooti", "child1i", "child2i"]
+ : ["child2s", "child1s", "values", "roots", "child2p", "child1p", "valuep", "rootp", "child2i", "child1i", "valuei", "rooti"];
+ BinaryTreeNodeWithEventsISerializable child2 = new() { Name = "child2" };
+ BinaryTreeNodeWithEventsISerializable child1 = new() { Name = "child1", Left = child2 };
+ BinaryTreeNodeWithEventsISerializable root = new() { Name = "root", Left = child1, Value = new ValueTypeISerializable() { Name = "value" } };
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth2_NoCycle_ISerializable_WithValueTypeISerializable_WithReference()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["p", "child2s", "values", "child1s", "roots", "valuep", "rootp", "child1p", "valuei", "rooti", "child1i", "child2i"]
+ : ["child2s", "child1s", "values", "roots", "child2p", "child1p", "valuep", "rootp", "child2i", "child1i", "valuei", "rooti"];
+ BinaryTreeNodeWithEventsISerializable child2 = new() { Name = "child2" };
+ BinaryTreeNodeWithEventsISerializable child1 = new() { Name = "child1", Left = child2 };
+ BinaryTreeNodeWithEventsISerializable root = new()
+ {
+ Name = "root",
+ Left = child1,
+ Value = new ValueTypeISerializable() { Name = "value", Reference = child2 }
+ };
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth2_NoCycle()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["child2p", "rootp", "child1p", "rooti", "child1i", "child2i"]
+ : ["child2p", "child1p", "rootp", "child2i", "child1i", "rooti"];
+ BinaryTreeNodeWithEvents child2 = new() { Name = "child2" };
+ BinaryTreeNodeWithEvents child1 = new() { Name = "child1", Left = child2 };
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Left = child1 };
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth2_NoCycle_Surrogate()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["p", "child2s", "child1s", "roots", "rootp", "child1p", "rooti", "child1i", "child2i"]
+ : ["child2s", "child1s", "roots", "child2p", "child1p", "rootp", "child2i", "child1i", "rooti"];
+ SurrogateSelector selector = CreateSurrogateSelector(new BinaryTreeNodeWithEventsSurrogate());
+ BinaryTreeNodeWithEvents child2 = new() { Name = "child2" };
+ BinaryTreeNodeWithEvents child1 = new() { Name = "child1", Left = child2 };
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Left = child1 };
+
+ try
+ {
+ Stream stream = Serialize(root);
+ Deserialize(stream, surrogateSelector: selector);
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth2_NoCycle_WithValueType()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["child2p", "valuep", "rootp", "child1p", "valuei", "rooti", "child1i", "child2i"]
+ : ["child2p", "child1p", "valuep", "rootp", "child2i", "child1i", "valuei", "rooti"];
+ BinaryTreeNodeWithEvents child2 = new() { Name = "child2" };
+ BinaryTreeNodeWithEvents child1 = new() { Name = "child1", Left = child2 };
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Left = child1, Value = new StructThatImplementsIDeserializationCallback() { Name = "value" } };
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth2_NoCycle_WithValueTypeISerializable()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["child2p", "values", "valuep", "rootp", "child1p", "valuei", "rooti", "child1i", "child2i"]
+ : ["values", "child2p", "child1p", "valuep", "rootp", "child2i", "child1i", "valuei", "rooti"];
+ BinaryTreeNodeWithEvents child2 = new() { Name = "child2" };
+ BinaryTreeNodeWithEvents child1 = new() { Name = "child1", Left = child2 };
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Left = child1, Value = new ValueTypeISerializable() { Name = "value" } };
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+ #endregion NoCycle
+
+ #region Cycle
+ [Fact]
+ public void Depth2_Cycle_ISerializable()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["p", "child2s", "child1s", "roots", "rootp", "child1p", "rooti", "child1i", "child2i"]
+ : ["child2s", "child1s", "roots", "child2p", "child1p", "rootp", "child2i", "child1i", "rooti"];
+ BinaryTreeNodeWithEventsISerializable child2 = new() { Name = "child2" };
+ BinaryTreeNodeWithEventsISerializable child1 = new() { Name = "child1", Left = child2 };
+ BinaryTreeNodeWithEventsISerializable root = new() { Name = "root", Left = child1 };
+ child2.Left = root;
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth2_Cycle_ISerializable_WithValueType()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["p", "child2s", "child1s", "roots", "value3p", "rootp", "value2p", "child1p", "value1p", "value3i", "rooti", "value2i", "child1i", "value1i", "child2i"]
+ : ["child2s", "child1s", "roots", "value1p", "value2p", "value3p", "child2p", "child1p", "rootp", "value1i", "value2i", "value3i", "child2i", "child1i", "rooti"];
+ BinaryTreeNodeWithEventsISerializable child2 = new() { Name = "child2", Value = new StructThatImplementsIDeserializationCallback() { Name = "value1" } };
+ BinaryTreeNodeWithEventsISerializable child1 = new() { Name = "child1", Left = child2, Value = new StructThatImplementsIDeserializationCallback() { Name = "value2" } };
+ BinaryTreeNodeWithEventsISerializable root = new() { Name = "root", Left = child1, Value = new StructThatImplementsIDeserializationCallback() { Name = "value3" } };
+ child2.Left = root;
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth2_Cycle_ISerializable_WithValueTypeISerializable()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["value3s", "value2s", "value1s", "child2s", "child1s", "roots", "value3p", "rootp", "value2p", "child1p", "value1p", "child2p", "value3i", "rooti", "value2i", "child1i", "value1i", "child2i" ]
+ : ["value1s", "child2s", "value2s", "child1s", "value3s", "roots", "value1p", "child2p", "value2p", "child1p", "value3p", "rootp", "value1i", "child2i", "value2i", "child1i", "value3i", "rooti"];
+ BinaryTreeNodeWithEventsISerializable child2 = new() { Name = "child2", Value = new ValueTypeISerializable() { Name = "value1" } };
+ BinaryTreeNodeWithEventsISerializable child1 = new() { Name = "child1", Left = child2, Value = new ValueTypeISerializable() { Name = "value2" } };
+ BinaryTreeNodeWithEventsISerializable root = new() { Name = "root", Left = child1, Value = new ValueTypeISerializable() { Name = "value3" } };
+ child2.Left = root;
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth2_Cycle()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["child2p", "rootp", "child1p", "rooti", "child1i", "child2i"]
+ : ["child2p", "child1p", "rootp", "child2i", "child1i", "rooti"];
+ BinaryTreeNodeWithEvents child2 = new() { Name = "child2" };
+ BinaryTreeNodeWithEvents child1 = new() { Name = "child1", Left = child2 };
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Left = child1 };
+ child2.Left = root;
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth2_Cycle_Surrogate()
+ {
+ SurrogateSelector selector = CreateSurrogateSelector(new BinaryTreeNodeWithEventsSurrogate());
+ BinaryTreeNodeWithEvents child2 = new() { Name = "child2" };
+ BinaryTreeNodeWithEvents child1 = new() { Name = "child1", Left = child2 };
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Left = child1 };
+ child2.Left = root;
+
+ try
+ {
+ Stream stream = Serialize(root);
+ if (IsBinaryFormatterDeserializer)
+ {
+ Action action = () => Deserialize(stream, surrogateSelector: selector);
+ action.Should().Throw();
+ }
+ else
+ {
+ Deserialize(stream, surrogateSelector: selector);
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal("child2s", "child1s", "roots", "child2p", "child1p", "rootp", "child2i", "child1i", "rooti");
+ }
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth2_Cycle_WithValueType()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["child2p", "value3p", "rootp", "value2p", "child1p", "value1p", "value3i", "rooti", "value2i", "child1i", "value1i", "child2i"]
+ : ["value1p", "child2p", "value2p", "child1p", "value3p", "rootp", "value1i", "child2i", "value2i", "child1i", "value3i", "rooti"];
+ BinaryTreeNodeWithEvents child2 = new() { Name = "child2", Value = new StructThatImplementsIDeserializationCallback() { Name = "value1" } };
+ BinaryTreeNodeWithEvents child1 = new() { Name = "child1", Left = child2, Value = new StructThatImplementsIDeserializationCallback() { Name = "value2" } };
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Left = child1, Value = new StructThatImplementsIDeserializationCallback() { Name = "value3" } };
+ child2.Left = root;
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+
+ [Fact]
+ public void Depth2_Cycle_WithValueTypeISerializable()
+ {
+ string[] expected = IsBinaryFormatterDeserializer
+ ? ["value3s", "value2s", "value1s", "value3p", "rootp", "value2p", "child1p", "value1p", "child2p", "value3i", "rooti", "value2i", "child1i", "value1i", "child2i"]
+ : ["value1s", "value2s", "value3s", "value1p", "child2p", "value2p", "child1p", "value3p", "rootp", "value1i", "child2i", "value2i", "child1i", "value3i", "rooti"];
+ BinaryTreeNodeWithEvents child2 = new() { Name = "child2", Value = new ValueTypeISerializable() { Name = "value1" } };
+ BinaryTreeNodeWithEvents child1 = new() { Name = "child1", Left = child2, Value = new ValueTypeISerializable() { Name = "value2" } };
+ BinaryTreeNodeWithEvents root = new() { Name = "root", Left = child1, Value = new ValueTypeISerializable() { Name = "value3" } };
+ child2.Left = root;
+
+ try
+ {
+ Deserialize(Serialize(root));
+ List deserializeOrder = BinaryTreeNodeWithEventsTracker.DeserializationOrder;
+ deserializeOrder.Should().Equal(expected);
+ }
+ finally
+ {
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Clear();
+ }
+ }
+ #endregion Cycle
+ #endregion Depth2
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/EventTests.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/EventTests.cs
new file mode 100644
index 0000000000000..429a060d9b81c
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/EventTests.cs
@@ -0,0 +1,132 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.Tests.Common;
+
+public abstract class EventTests : SerializationTest where T : ISerializer
+{
+ [Fact]
+ public void SerializationEvents_FireAsExpected()
+ {
+ IncrementCountsDuringRoundtrip obj = new (null);
+
+ Assert.Equal(0, obj.IncrementedDuringOnSerializingMethod);
+ Assert.Equal(0, obj.IncrementedDuringOnSerializedMethod);
+ Assert.Equal(0, obj.IncrementedDuringOnDeserializingMethod);
+ Assert.Equal(0, obj.IncrementedDuringOnDeserializedMethod);
+
+ Stream stream = Serialize(obj);
+
+ Assert.Equal(1, obj.IncrementedDuringOnSerializingMethod);
+ Assert.Equal(1, obj.IncrementedDuringOnSerializedMethod);
+ Assert.Equal(0, obj.IncrementedDuringOnDeserializingMethod);
+ Assert.Equal(0, obj.IncrementedDuringOnDeserializedMethod);
+
+ var result = (IncrementCountsDuringRoundtrip)Deserialize(stream);
+
+ Assert.Equal(1, obj.IncrementedDuringOnSerializingMethod);
+ Assert.Equal(1, obj.IncrementedDuringOnSerializedMethod);
+ Assert.Equal(0, obj.IncrementedDuringOnDeserializingMethod);
+ Assert.Equal(0, obj.IncrementedDuringOnDeserializedMethod);
+
+ Assert.Equal(1, result.IncrementedDuringOnSerializingMethod);
+ Assert.Equal(0, result.IncrementedDuringOnSerializedMethod);
+ Assert.Equal(1, result.IncrementedDuringOnDeserializingMethod);
+ Assert.Equal(1, result.IncrementedDuringOnDeserializedMethod);
+ }
+
+ [Fact]
+ public void SerializationEvents_DerivedTypeWithEvents_FireAsExpected()
+ {
+ DerivedIncrementCountsDuringRoundtrip obj = new(null);
+
+ Assert.Equal(0, obj.IncrementedDuringOnSerializingMethod);
+ Assert.Equal(0, obj.IncrementedDuringOnSerializedMethod);
+ Assert.Equal(0, obj.IncrementedDuringOnDeserializingMethod);
+ Assert.Equal(0, obj.IncrementedDuringOnDeserializedMethod);
+ Assert.Equal(0, obj._derivedIncrementedDuringOnSerializingMethod);
+ Assert.Equal(0, obj._derivedIncrementedDuringOnSerializedMethod);
+ Assert.Equal(0, obj._derivedIncrementedDuringOnDeserializingMethod);
+ Assert.Equal(0, obj._derivedIncrementedDuringOnDeserializedMethod);
+
+ Stream stream = Serialize(obj);
+
+ Assert.Equal(1, obj.IncrementedDuringOnSerializingMethod);
+ Assert.Equal(1, obj.IncrementedDuringOnSerializedMethod);
+ Assert.Equal(0, obj.IncrementedDuringOnDeserializingMethod);
+ Assert.Equal(0, obj.IncrementedDuringOnDeserializedMethod);
+ Assert.Equal(1, obj._derivedIncrementedDuringOnSerializingMethod);
+ Assert.Equal(1, obj._derivedIncrementedDuringOnSerializedMethod);
+ Assert.Equal(0, obj._derivedIncrementedDuringOnDeserializingMethod);
+ Assert.Equal(0, obj._derivedIncrementedDuringOnDeserializedMethod);
+
+ var result = (DerivedIncrementCountsDuringRoundtrip)Deserialize(stream);
+
+ Assert.Equal(1, obj.IncrementedDuringOnSerializingMethod);
+ Assert.Equal(1, obj.IncrementedDuringOnSerializedMethod);
+ Assert.Equal(0, obj.IncrementedDuringOnDeserializingMethod);
+ Assert.Equal(0, obj.IncrementedDuringOnDeserializedMethod);
+ Assert.Equal(1, obj._derivedIncrementedDuringOnSerializingMethod);
+ Assert.Equal(1, obj._derivedIncrementedDuringOnSerializedMethod);
+ Assert.Equal(0, obj._derivedIncrementedDuringOnDeserializingMethod);
+ Assert.Equal(0, obj._derivedIncrementedDuringOnDeserializedMethod);
+
+ Assert.Equal(1, result.IncrementedDuringOnSerializingMethod);
+ Assert.Equal(0, result.IncrementedDuringOnSerializedMethod);
+ Assert.Equal(1, result.IncrementedDuringOnDeserializingMethod);
+ Assert.Equal(1, result.IncrementedDuringOnDeserializedMethod);
+ Assert.Equal(1, result._derivedIncrementedDuringOnSerializingMethod);
+ Assert.Equal(0, result._derivedIncrementedDuringOnSerializedMethod);
+ Assert.Equal(1, result._derivedIncrementedDuringOnDeserializingMethod);
+ Assert.Equal(1, result._derivedIncrementedDuringOnDeserializedMethod);
+ }
+
+ [Serializable]
+ public class IncrementCountsDuringRoundtrip
+ {
+ public int IncrementedDuringOnSerializingMethod;
+ public int IncrementedDuringOnSerializedMethod;
+ [NonSerialized] public int IncrementedDuringOnDeserializingMethod;
+ public int IncrementedDuringOnDeserializedMethod;
+
+ // non-default ctor so that we can observe changes from OnDeserializing
+ public IncrementCountsDuringRoundtrip(string? ignored) { _ = ignored; }
+
+ [OnSerializing]
+ private void OnSerializingMethod(StreamingContext context) => IncrementedDuringOnSerializingMethod++;
+
+ [OnSerialized]
+ private void OnSerializedMethod(StreamingContext context) => IncrementedDuringOnSerializedMethod++;
+
+ [OnDeserializing]
+ private void OnDeserializingMethod(StreamingContext context) => IncrementedDuringOnDeserializingMethod++;
+
+ [OnDeserialized]
+ private void OnDeserializedMethod(StreamingContext context) => IncrementedDuringOnDeserializedMethod++;
+ }
+
+ [Serializable]
+ public sealed class DerivedIncrementCountsDuringRoundtrip : IncrementCountsDuringRoundtrip
+ {
+ internal int _derivedIncrementedDuringOnSerializingMethod;
+ internal int _derivedIncrementedDuringOnSerializedMethod;
+ [NonSerialized] internal int _derivedIncrementedDuringOnDeserializingMethod;
+ internal int _derivedIncrementedDuringOnDeserializedMethod;
+
+ public DerivedIncrementCountsDuringRoundtrip(string? ignored) : base(ignored) { }
+
+ [OnSerializing]
+ private void OnSerializingMethod(StreamingContext context) => _derivedIncrementedDuringOnSerializingMethod++;
+
+ [OnSerialized]
+ private void OnSerializedMethod(StreamingContext context) => _derivedIncrementedDuringOnSerializedMethod++;
+
+ [OnDeserializing]
+ private void OnDeserializingMethod(StreamingContext context) => _derivedIncrementedDuringOnDeserializingMethod++;
+
+ [OnDeserialized]
+ private void OnDeserializedMethod(StreamingContext context) => _derivedIncrementedDuringOnDeserializedMethod++;
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/FieldTests.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/FieldTests.cs
new file mode 100644
index 0000000000000..0a29342050c3b
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/FieldTests.cs
@@ -0,0 +1,69 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.Formatters;
+
+namespace System.Resources.Extensions.Tests.Common;
+
+public abstract class FieldTests : SerializationTest where T : ISerializer
+{
+ [Theory]
+ [InlineData(FormatterAssemblyStyle.Simple, false)]
+ [InlineData(FormatterAssemblyStyle.Full, true)]
+ public void MissingField_FailsWithAppropriateStyle(FormatterAssemblyStyle assemblyMatching, bool exceptionExpected)
+ {
+ Stream stream = Serialize(new Version1ClassWithoutField());
+
+ var binder = new DelegateBinder
+ {
+ BindToTypeDelegate = (_, _) => typeof(Version2ClassWithoutOptionalField)
+ };
+
+ if (exceptionExpected)
+ {
+ Assert.Throws(() => Deserialize(stream, binder, assemblyMatching));
+ }
+ else
+ {
+ var result = (Version2ClassWithoutOptionalField)Deserialize(stream, binder, assemblyMatching);
+ Assert.NotNull(result);
+ Assert.Null(result.Value);
+ }
+ }
+
+ [Theory]
+ [InlineData(FormatterAssemblyStyle.Simple)]
+ [InlineData(FormatterAssemblyStyle.Full)]
+ public void OptionalField_Missing_Success(FormatterAssemblyStyle assemblyMatching)
+ {
+ Stream stream = Serialize(new Version1ClassWithoutField());
+
+ var binder = new DelegateBinder
+ {
+ BindToTypeDelegate = (_, _) => typeof(Version2ClassWithOptionalField)
+ };
+
+ var result = (Version2ClassWithOptionalField)Deserialize(stream, binder, assemblyMatching);
+ Assert.NotNull(result);
+ Assert.Null(result.Value);
+ }
+
+ [Serializable]
+ public class Version1ClassWithoutField
+ {
+ }
+
+ [Serializable]
+ public class Version2ClassWithoutOptionalField
+ {
+ public object? Value;
+ }
+
+ [Serializable]
+ public class Version2ClassWithOptionalField
+ {
+ [OptionalField(VersionAdded = 2)]
+ public object? Value;
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/ISerializer.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/ISerializer.cs
new file mode 100644
index 0000000000000..6c4fe51be5826
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/ISerializer.cs
@@ -0,0 +1,36 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.Formatters;
+using System.Runtime.Serialization.Formatters.Binary;
+
+namespace System.Resources.Extensions.Tests.Common;
+
+public interface ISerializer
+{
+ static virtual Stream Serialize(
+ object value,
+ SerializationBinder? binder = null,
+ ISurrogateSelector? surrogateSelector = null,
+ FormatterTypeStyle typeStyle = FormatterTypeStyle.TypesAlways)
+ {
+ MemoryStream stream = new();
+ BinaryFormatter formatter = new()
+ {
+ SurrogateSelector = surrogateSelector,
+ TypeFormat = typeStyle,
+ Binder = binder
+ };
+
+ formatter.Serialize(stream, value);
+ stream.Position = 0;
+ return stream;
+ }
+
+ static abstract object Deserialize(
+ Stream stream,
+ SerializationBinder? binder = null,
+ FormatterAssemblyStyle assemblyMatching = FormatterAssemblyStyle.Simple,
+ ISurrogateSelector? surrogateSelector = null);
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/JaggedArrayTests.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/JaggedArrayTests.cs
new file mode 100644
index 0000000000000..ab47e6b5f448f
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/JaggedArrayTests.cs
@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Resources.Extensions.Tests.Common;
+
+public abstract class JaggedArrayTests : SerializationTest where T : ISerializer
+{
+ [Fact]
+ public void IntegerArraysTwoLevels()
+ {
+ int[][] jaggedArray = [[0, 1], [10, 11]];
+
+ Stream stream = Serialize(jaggedArray);
+ object deserialized = Deserialize(stream);
+
+ deserialized.Should().BeEquivalentTo(jaggedArray);
+ }
+
+ [Fact]
+ public void IntegerArraysThreeLevels()
+ {
+ int[][][] jaggedArray = [[[0, 1], [10, 11]], [[100, 101], [110, 111]]];
+
+ Stream stream = Serialize(jaggedArray);
+ object deserialized = Deserialize(stream);
+ deserialized.Should().BeEquivalentTo(jaggedArray);
+ }
+
+ [Fact]
+ public void JaggedEmpty()
+ {
+ int[][] jaggedEmpty = new int[1][];
+
+ object deserialized = Deserialize(Serialize(jaggedEmpty));
+ deserialized.Should().BeEquivalentTo(jaggedEmpty);
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/MultidimensionalArrayTests.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/MultidimensionalArrayTests.cs
new file mode 100644
index 0000000000000..20c25f44f6063
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/MultidimensionalArrayTests.cs
@@ -0,0 +1,127 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+namespace System.Resources.Extensions.Tests.Common;
+
+public abstract class MultidimensionalArrayTests : SerializationTest where T : ISerializer
+{
+ [Fact]
+ public void StringArrays()
+ {
+ string[,] twoDimensions = new string[2, 2];
+ twoDimensions[0, 0] = "00";
+ twoDimensions[0, 1] = "01";
+ twoDimensions[1, 0] = "10";
+ twoDimensions[1, 1] = "11";
+
+ // Raw data will be 0, 1, 10, 11 in memory and in the binary stream
+ Stream stream = Serialize(twoDimensions);
+
+ object deserialized = Deserialize(stream);
+ deserialized.Should().BeEquivalentTo(twoDimensions);
+ }
+
+ [Fact]
+ public void IntegerArrays_Basic()
+ {
+ int[,] twoDimensions = new int[2, 2];
+ twoDimensions[0, 0] = 0;
+ twoDimensions[0, 1] = 1;
+ twoDimensions[1, 0] = 10;
+ twoDimensions[1, 1] = 11;
+
+ // Raw data will be 0, 1, 10, 11 in memory and in the binary stream
+ object deserialized = Deserialize(Serialize(twoDimensions));
+
+ deserialized.Should().BeEquivalentTo(twoDimensions);
+
+ int[,,] threeDimensions = new int[2, 2, 2];
+ threeDimensions[0, 0, 0] = 888;
+ threeDimensions[0, 0, 1] = 881;
+ threeDimensions[0, 1, 0] = 810;
+ threeDimensions[0, 1, 1] = 811;
+ threeDimensions[1, 0, 0] = 100;
+ threeDimensions[1, 0, 1] = 101;
+ threeDimensions[1, 1, 0] = 110;
+ threeDimensions[1, 1, 1] = 111;
+
+ deserialized = Deserialize(Serialize(threeDimensions));
+ deserialized.Should().BeEquivalentTo(threeDimensions);
+ }
+
+ [Fact]
+ public void EmptyDimensions()
+ {
+ // Didn't even know this was possible.
+ int[,] twoDimensionOneEmpty = new int[1, 0];
+ object deserialized = Deserialize(Serialize(twoDimensionOneEmpty));
+ deserialized.Should().BeEquivalentTo(twoDimensionOneEmpty);
+
+ int[,] twoDimensionEmptyOne = new int[0, 1];
+ deserialized = Deserialize(Serialize(twoDimensionEmptyOne));
+ deserialized.Should().BeEquivalentTo(twoDimensionEmptyOne);
+
+ int[,] twoDimensionEmpty = new int[0, 0];
+ deserialized = Deserialize(Serialize(twoDimensionEmpty));
+ deserialized.Should().BeEquivalentTo(twoDimensionEmpty);
+
+ int[,,] threeDimension = new int[1, 0, 1];
+ deserialized = Deserialize(Serialize(threeDimension));
+ deserialized.Should().BeEquivalentTo(threeDimension);
+ }
+
+ [Theory]
+ [MemberData(nameof(DimensionLengthsTestData))]
+ public void IntegerArrays(int[] lengths)
+ {
+ Array array = Array.CreateInstance(typeof(int), lengths);
+
+ InitArray(array);
+
+ Array deserialized = (Array)Deserialize(Serialize(array));
+ deserialized.Should().BeEquivalentTo(deserialized);
+ }
+
+ public static TheoryData DimensionLengthsTestData { get; } = new()
+ {
+ new int[] { 2, 2 },
+ new int[] { 3, 2 },
+ new int[] { 2, 3 },
+ new int[] { 3, 3, 3 },
+ new int[] { 2, 3, 4 },
+ new int[] { 4, 3, 2 },
+ new int[] { 2, 3, 4, 5 }
+ };
+
+ [Fact]
+ public void MaxDimensions()
+ {
+ int[] lengths = new int[32];
+
+ // Even at 2 in every dimension it would be uint.MaxValue in LongLength and 16GB of memory.
+ lengths.AsSpan().Fill(1);
+ Array array = Array.CreateInstance(typeof(int), lengths);
+
+ InitArray(array);
+
+ Array deserialized = (Array)Deserialize(Serialize(array));
+ deserialized.Should().BeEquivalentTo(deserialized);
+ }
+
+ private static void InitArray(Array array)
+ {
+ ref byte arrayDataRef = ref MemoryMarshal.GetArrayDataReference(array);
+ ref int elementRef = ref Unsafe.As(ref arrayDataRef);
+ nuint flattenedIndex = 0;
+
+ for (int i = 0; i < array.LongLength; i++)
+ {
+ ref int offsetElementRef = ref Unsafe.Add(ref elementRef, flattenedIndex);
+ offsetElementRef = i;
+ flattenedIndex++;
+ }
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/ObjectReferenceTests.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/ObjectReferenceTests.cs
new file mode 100644
index 0000000000000..3c363e6657f77
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/ObjectReferenceTests.cs
@@ -0,0 +1,128 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+using System.Text;
+
+namespace System.Resources.Extensions.Tests.Common;
+
+public abstract class ObjectReferenceTests : SerializationTest where T : ISerializer
+{
+ [Fact]
+ public void DBNull_Deserialize()
+ {
+ object deserialized = RoundTrip(DBNull.Value);
+ deserialized.Should().BeSameAs(DBNull.Value);
+ }
+
+ [Theory]
+ [MemberData(nameof(SupportedTypesTestData))]
+ public void SupportedTypes_Deserialize(object value)
+ {
+ object deserialized = RoundTrip(value);
+ deserialized.Should().NotBeNull();
+ }
+
+ public static TheoryData SupportedTypesTestData { get; } = new()
+ {
+ ObjectReferenceNoFields.Value,
+ new SerializableWithNestedSurrogate { Message = "Hello"}
+ };
+
+ [Fact]
+ public void Singleton_NoFields_Deserialize()
+ {
+ // Representing singletons is the most common pattern for IObjectReference.
+ object deserialized = RoundTrip(ObjectReferenceNoFields.Value);
+ deserialized.Should().BeSameAs(ObjectReferenceNoFields.Value);
+ }
+
+ [Serializable]
+ public sealed class ObjectReferenceNoFields : IObjectReference
+ {
+ public static ObjectReferenceNoFields Value { get; } = new();
+
+ private ObjectReferenceNoFields() { }
+
+ object IObjectReference.GetRealObject(StreamingContext context) => Value;
+ }
+
+ [Fact]
+ public void NestedSurrogate_Deserialize()
+ {
+ object deserialized = RoundTrip(new SerializableWithNestedSurrogate { Message = "Hello" });
+ deserialized.Should().BeOfType().Which.Message.Should().Be("Hello");
+ }
+
+ [Serializable]
+#pragma warning disable CA2229 // Implement serialization constructors
+ public sealed class SerializableWithNestedSurrogate : ISerializable
+#pragma warning restore CA2229
+ {
+ public string Message { get; set; } = string.Empty;
+
+ void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ info.SetType(typeof(SerializationSurrogate));
+ info.AddValue(nameof(Message), Encoding.UTF8.GetBytes(Message));
+ }
+
+ [Serializable]
+ private sealed class SerializationSurrogate : IObjectReference, ISerializable
+ {
+ private readonly byte[] _bytes;
+
+ private SerializationSurrogate(SerializationInfo info, StreamingContext context)
+ {
+ _bytes = (byte[])(info.GetValue(nameof(Message), typeof(byte[])) ?? throw new InvalidOperationException());
+ }
+
+ void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) =>
+ throw new InvalidOperationException();
+
+ object IObjectReference.GetRealObject(StreamingContext context)
+ => new SerializableWithNestedSurrogate() { Message = Encoding.UTF8.GetString(_bytes) };
+ }
+ }
+
+ [Fact]
+ public void NestedSurrogate_NullableEnum_Deserialize()
+ {
+ object deserialized = RoundTrip(new SerializableWithNestedSurrogate_NullableEnum());
+ deserialized.Should().BeOfType().Which.Day.Should().BeNull();
+
+ deserialized = RoundTrip(new SerializableWithNestedSurrogate_NullableEnum { Day = DayOfWeek.Monday });
+ deserialized.Should().BeOfType().Which.Day.Should().Be(DayOfWeek.Monday);
+ }
+
+ [Serializable]
+#pragma warning disable CA2229 // Implement serialization constructors
+ public sealed class SerializableWithNestedSurrogate_NullableEnum : ISerializable
+#pragma warning restore CA2229
+ {
+ public DayOfWeek? Day { get; set; }
+
+ void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ info.SetType(typeof(SerializationSurrogate));
+ info.AddValue(nameof(Day), Day);
+ }
+
+ [Serializable]
+ private sealed class SerializationSurrogate : IObjectReference, ISerializable
+ {
+ private readonly DayOfWeek? _day;
+
+ private SerializationSurrogate(SerializationInfo info, StreamingContext context)
+ {
+ _day = (DayOfWeek?)(info.GetValue(nameof(Day), typeof(DayOfWeek?)));
+ }
+
+ void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context) =>
+ throw new InvalidOperationException();
+
+ object IObjectReference.GetRealObject(StreamingContext context)
+ => new SerializableWithNestedSurrogate_NullableEnum() { Day = _day };
+ }
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/PrimitiveTests.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/PrimitiveTests.cs
new file mode 100644
index 0000000000000..a8bb284c3c889
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/PrimitiveTests.cs
@@ -0,0 +1,8 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Resources.Extensions.Tests.Common;
+
+public abstract class PrimitiveTests : SerializationTest where T : ISerializer
+{
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/SerializationTest.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/SerializationTest.cs
new file mode 100644
index 0000000000000..3a7b404605883
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/SerializationTest.cs
@@ -0,0 +1,136 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Buffers;
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.BinaryFormat;
+using System.Runtime.Serialization.Formatters;
+
+namespace System.Resources.Extensions.Tests.Common;
+
+public abstract class SerializationTest where TSerializer : ISerializer
+{
+ public static TheoryData FormatterOptions => new()
+ {
+ // XsdString always writes strings inline (never as a record). Despite FormatterTypeStyle
+ // not having [Flags] it is treated as flags in the serializer. If you don't explicitly set
+ // TypesAlways, TypesWhenNeeded is the default.
+ { FormatterTypeStyle.TypesWhenNeeded, FormatterAssemblyStyle.Full },
+ { FormatterTypeStyle.TypesWhenNeeded, FormatterAssemblyStyle.Simple },
+ { FormatterTypeStyle.TypesAlways, FormatterAssemblyStyle.Full },
+ { FormatterTypeStyle.TypesAlways, FormatterAssemblyStyle.Simple },
+ { FormatterTypeStyle.TypesAlways | FormatterTypeStyle.XsdString, FormatterAssemblyStyle.Full },
+ { FormatterTypeStyle.TypesAlways | FormatterTypeStyle.XsdString, FormatterAssemblyStyle.Simple },
+ { FormatterTypeStyle.TypesWhenNeeded | FormatterTypeStyle.XsdString, FormatterAssemblyStyle.Full },
+ { FormatterTypeStyle.TypesWhenNeeded | FormatterTypeStyle.XsdString, FormatterAssemblyStyle.Simple },
+ };
+
+ private protected static Stream Serialize(
+ object value,
+ SerializationBinder? binder = null,
+ ISurrogateSelector? surrogateSelector = null,
+ FormatterTypeStyle typeStyle = FormatterTypeStyle.TypesAlways) =>
+ TSerializer.Serialize(value, binder, surrogateSelector, typeStyle);
+
+ private protected static object Deserialize(
+ Stream stream,
+ SerializationBinder? binder = null,
+ FormatterAssemblyStyle assemblyMatching = FormatterAssemblyStyle.Simple,
+ ISurrogateSelector? surrogateSelector = null) =>
+ TSerializer.Deserialize(stream, binder, assemblyMatching, surrogateSelector);
+
+ private protected static TObject RoundTrip(
+ TObject value,
+ SerializationBinder? binder = null,
+ ISurrogateSelector? surrogateSelector = null,
+ FormatterTypeStyle typeStyle = FormatterTypeStyle.TypesAlways,
+ FormatterAssemblyStyle assemblyMatching = FormatterAssemblyStyle.Simple) where TObject : notnull
+ {
+ // TODO: Use array pool
+ return (TObject)Deserialize(Serialize(value, binder, surrogateSelector, typeStyle), binder, assemblyMatching, surrogateSelector);
+ }
+
+ private protected static object DeserializeFromBase64Chars(
+ ReadOnlySpan chars,
+ SerializationBinder? binder = null,
+ FormatterAssemblyStyle assemblyMatching = FormatterAssemblyStyle.Simple,
+ ISurrogateSelector? surrogateSelector = null)
+ {
+ byte[] buffer = ArrayPool.Shared.Rent(chars.Length);
+ if (!Convert.TryFromBase64Chars(chars, buffer, out _))
+ {
+ throw new InvalidOperationException();
+ }
+
+ MemoryStream stream = new(buffer);
+ try
+ {
+ return Deserialize(stream, binder, assemblyMatching, surrogateSelector);
+ }
+ finally
+ {
+ stream.Dispose();
+ ArrayPool.Shared.Return(buffer);
+ }
+ }
+
+ private protected static SurrogateSelector CreateSurrogateSelector(ISerializationSurrogate surrogate)
+ {
+ SurrogateSelector selector = new();
+ selector.AddSurrogate(
+ typeof(TSurrogated),
+ new StreamingContext(StreamingContextStates.All),
+ surrogate);
+
+ return selector;
+ }
+
+ public static bool IsBinaryFormatterDeserializer => false;
+
+ protected static void WriteSerializedStreamHeader(BinaryWriter writer, int major = 1, int minor = 0)
+ {
+ writer.Write((byte)RecordType.SerializedStreamHeader);
+ writer.Write(1); // root ID
+ writer.Write(1); // header ID
+ writer.Write(major); // major version
+ writer.Write(minor); // minor version
+ }
+
+ protected static void WriteBinaryLibrary(BinaryWriter writer, int objectId, string libraryName)
+ {
+ writer.Write((byte)RecordType.BinaryLibrary);
+ writer.Write(objectId);
+ writer.Write(libraryName);
+ }
+
+ protected static void WriteClassInfo(BinaryWriter writer, int objectId, string typeName, params string[] memberNames)
+ {
+ writer.Write((byte)RecordType.ClassWithMembersAndTypes);
+ writer.Write(objectId);
+ writer.Write(typeName);
+ writer.Write(memberNames.Length);
+
+ foreach (string memberName in memberNames)
+ {
+ writer.Write(memberName);
+ }
+ }
+
+ protected static void WriteClassFieldInfo(BinaryWriter writer, string typeName, int libraryId)
+ {
+ writer.Write((byte)BinaryType.Class);
+ writer.Write(typeName);
+ writer.Write(libraryId);
+ }
+
+ protected static void WriteMemberReference(BinaryWriter writer, int referencedObjectId)
+ {
+ writer.Write((byte)RecordType.MemberReference);
+ writer.Write(referencedObjectId);
+ }
+
+ protected static void WriteMessageEnd(BinaryWriter writer)
+ {
+ writer.Write((byte)RecordType.MessageEnd);
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/StressTests.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/StressTests.cs
new file mode 100644
index 0000000000000..b04cec200cc49
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/StressTests.cs
@@ -0,0 +1,29 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Resources.Extensions.Tests.Common.TestTypes;
+
+namespace System.Resources.Extensions.Tests.Common;
+
+public abstract class StressTests : SerializationTest where T : ISerializer
+{
+ [Theory]
+ [InlineData(1000)]
+ [InlineData(10000)]
+ // This takes a few seconds
+ // [InlineData(100000)]
+ public void GraphDepth(int depth)
+ {
+ SimpleNode root = new();
+ SimpleNode current = root;
+ for (int i = 1; i < depth; i++)
+ {
+ current.Next = new();
+ current = current.Next;
+ }
+
+ SimpleNode deserialized = (SimpleNode)Deserialize(Serialize(root));
+ deserialized.Next.Should().NotBeNull();
+ deserialized.Next.Should().NotBeSameAs(deserialized);
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/SurrogateTests.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/SurrogateTests.cs
new file mode 100644
index 0000000000000..2c765a3da2e06
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/SurrogateTests.cs
@@ -0,0 +1,118 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Drawing;
+using System.Runtime.CompilerServices;
+using System.Runtime.Serialization;
+using System.Resources.Extensions.Tests.Common.TestTypes;
+
+namespace System.Resources.Extensions.Tests.Common;
+
+public abstract class SurrogateTests : SerializationTest where T : ISerializer
+{
+ [Fact]
+ public void SerializePointWithSurrogate_NonRefChange()
+ {
+ Point point = new(42, 43);
+
+ // Surrogates cannot change the equality of types that they change. BinaryFormattedObject would allow this
+ // unless you wrap the selector by calling FormatterServices.GetSurrogateForCyclicalReference. Ours always
+ // allows value types to change as they are always applied as a fixup.
+ SurrogateSelector selector = CreateSurrogateSelector(new PointSerializationSurrogate(refUnbox: false));
+
+ Stream stream = Serialize(point);
+ Deserialize(stream, surrogateSelector: selector);
+ }
+
+ [Fact]
+ public void SerializePointWithSurrogate_RefChange()
+ {
+ Point point = new(42, 43);
+
+ Stream stream = Serialize(point);
+
+ // Surrogates can change the equality of the structs they are passed
+ // if they unbox as ref (using Unsafe).
+ SurrogateSelector selector = CreateSurrogateSelector(new PointSerializationSurrogate(refUnbox: true));
+
+ Point deserialized = (Point)Deserialize(stream, surrogateSelector: selector);
+ deserialized.Should().Be(point);
+ }
+
+ public class PointSerializationSurrogate : ISerializationSurrogate
+ {
+ private readonly bool _refUnbox;
+
+ public PointSerializationSurrogate(bool refUnbox) => _refUnbox = refUnbox;
+
+ public void GetObjectData(object obj, SerializationInfo info, StreamingContext context) =>
+ throw new NotImplementedException();
+
+ public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector? selector)
+ {
+ if (_refUnbox)
+ {
+ ref Point pointRef = ref Unsafe.Unbox(obj);
+ pointRef.X = info.GetInt32("x");
+ pointRef.Y = info.GetInt32("y");
+ return obj;
+ }
+
+ Point point = (Point)obj;
+ point.X = info.GetInt32("x");
+ point.Y = info.GetInt32("y");
+ return point;
+ }
+ }
+
+ [Fact]
+ public void SerializePointWithNullSurrogate()
+ {
+ Point point = new(42, 43);
+
+ Stream stream = Serialize(point);
+
+ // Not sure why one would want to do this, but returning null will skip setting the value back.
+ SurrogateSelector selector = CreateSurrogateSelector(new NullSurrogate());
+ Point deserialized = (Point)Deserialize(stream, surrogateSelector: selector);
+
+ deserialized.X.Should().Be(0);
+ deserialized.Y.Should().Be(0);
+ }
+
+ public class NullSurrogate : ISerializationSurrogate
+ {
+ public void GetObjectData(object obj, SerializationInfo info, StreamingContext context) =>
+ throw new NotImplementedException();
+
+ public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector? selector) =>
+ null!;
+ }
+
+ public class EqualsButDifferentSurrogate : ISerializationSurrogate
+ {
+ public void GetObjectData(object obj, SerializationInfo info, StreamingContext context) =>
+ throw new NotImplementedException();
+
+ public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector? selector)
+ {
+ return null!;
+ }
+ }
+
+ [Fact]
+ public void SerializeNonSerializableTypeWithSurrogate()
+ {
+ NonSerializablePair pair = new() { Value1 = 1, Value2 = "2" };
+ pair.GetType().IsSerializable.Should().BeFalse();
+ Action action = () => Serialize(pair);
+ action.Should().Throw();
+
+ SurrogateSelector selector = CreateSurrogateSelector>(new NonSerializablePairSurrogate());
+
+ var deserialized = RoundTrip(pair, surrogateSelector: selector);
+ deserialized.Should().NotBeSameAs(pair);
+ deserialized.Value1.Should().Be(pair.Value1);
+ deserialized.Value2.Should().Be(pair.Value2);
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/SystemDrawingTests.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/SystemDrawingTests.cs
new file mode 100644
index 0000000000000..81b491e887edb
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/SystemDrawingTests.cs
@@ -0,0 +1,34 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Drawing;
+using System.Runtime.Serialization.Formatters;
+using System.Runtime.Versioning;
+
+namespace System.Resources.Extensions.Tests.Common;
+
+[ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.IsDrawingSupported))]
+public abstract class SystemDrawingTests : SerializationTest where T : ISerializer
+{
+ [Theory]
+ [MemberData(nameof(FormatterOptions))]
+ [SupportedOSPlatform("windows")]
+ public void Bitmap_RoundTrip(FormatterTypeStyle typeStyle, FormatterAssemblyStyle assemblyMatching)
+ {
+ using Bitmap bitmap = new(10, 10);
+ using var deserialized = RoundTrip(bitmap, typeStyle: typeStyle, assemblyMatching: assemblyMatching);
+ deserialized.Size.Should().Be(bitmap.Size);
+ }
+
+ [Theory]
+ [MemberData(nameof(FormatterOptions))]
+ [SupportedOSPlatform("windows")]
+ public void Png_RoundTrip(FormatterTypeStyle typeStyle, FormatterAssemblyStyle assemblyMatching)
+ {
+ byte[] rawInlineImageBytes = Convert.FromBase64String(TestResources.TestPng);
+ using Bitmap bitmap = new(new MemoryStream(rawInlineImageBytes));
+ using var deserialized = RoundTrip(bitmap, typeStyle: typeStyle, assemblyMatching: assemblyMatching);
+ deserialized.Size.Should().Be(bitmap.Size);
+ deserialized.RawFormat.Should().Be(bitmap.RawFormat);
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/BasicISerializableObject.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/BasicISerializableObject.cs
new file mode 100644
index 0000000000000..75a13841d8892
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/BasicISerializableObject.cs
@@ -0,0 +1,39 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.Tests.Common.TestTypes;
+
+[Serializable]
+public class BasicISerializableObject : ISerializable
+{
+ private readonly NonSerializablePair _data;
+
+ public BasicISerializableObject(int value1, string value2)
+ {
+ _data = new NonSerializablePair { Value1 = value1, Value2 = value2 };
+ }
+
+ protected BasicISerializableObject(SerializationInfo info, StreamingContext context)
+ {
+ _data = new NonSerializablePair { Value1 = info.GetInt32("Value1"), Value2 = info.GetString("Value2")! };
+ }
+
+ public void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ info.AddValue("Value1", _data.Value1);
+ info.AddValue("Value2", _data.Value2);
+ }
+
+ public override bool Equals(object? obj)
+ {
+ if (obj is not BasicISerializableObject o)
+ return false;
+ if (_data is null || o._data is null)
+ return _data == o._data;
+ return _data.Value1 == o._data.Value1 && _data.Value2 == o._data.Value2;
+ }
+
+ public override int GetHashCode() => 1;
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/BinaryTreeNode.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/BinaryTreeNode.cs
new file mode 100644
index 0000000000000..dc7a0b9e68685
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/BinaryTreeNode.cs
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Resources.Extensions.Tests.Common.TestTypes;
+
+[Serializable]
+public class BinaryTreeNode
+{
+ public BinaryTreeNode? Left { get; set; }
+ public BinaryTreeNode? Right { get; set; }
+ public string? Value { get; set; }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/BinaryTreeNodeISerializable.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/BinaryTreeNodeISerializable.cs
new file mode 100644
index 0000000000000..b05762413e92c
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/BinaryTreeNodeISerializable.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.Tests.Common.TestTypes;
+
+[Serializable]
+public class BinaryTreeNodeISerializable : BinaryTreeNode, ISerializable
+{
+ public BinaryTreeNodeISerializable() { }
+
+ protected BinaryTreeNodeISerializable(SerializationInfo info, StreamingContext context)
+ {
+ Value = info.GetString(nameof(Value));
+ Left = (BinaryTreeNode)info.GetValue(nameof(Left), typeof(BinaryTreeNode))!;
+ Right = (BinaryTreeNode)info.GetValue(nameof(Right), typeof(BinaryTreeNode))!;
+ }
+
+ public void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ info.AddValue("Value", Value);
+ info.AddValue("Left", Left, typeof(BinaryTreeNode));
+ info.AddValue("Right", Right, typeof(BinaryTreeNode));
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/BinaryTreeNodeWithEvents.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/BinaryTreeNodeWithEvents.cs
new file mode 100644
index 0000000000000..11097a329fd12
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/BinaryTreeNodeWithEvents.cs
@@ -0,0 +1,46 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.Tests.Common.TestTypes;
+
+[Serializable]
+public class BinaryTreeNodeWithEvents : IDeserializationCallback, BinaryTreeNodeWithEventsBase
+{
+ public string Name { get; set; } = string.Empty;
+ public BinaryTreeNodeWithEvents? Left { get; set; }
+ public BinaryTreeNodeWithEvents? Right { get; set; }
+ public ValueTypeBase? Value { get; set; }
+
+ public BinaryTreeNodeWithEvents() { }
+
+ [OnDeserialized]
+ private void OnDeserialized(StreamingContext context) => BinaryTreeNodeWithEventsTracker.DeserializationOrder.Add($"{Name}p");
+
+ public void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ info.AddValue(nameof(Name), Name);
+ info.AddValue(nameof(Left), Left);
+ info.AddValue(nameof(Right), Right);
+ info.AddValue(nameof(Value), Value);
+ }
+
+ public void OnDeserialization(object? sender) => BinaryTreeNodeWithEventsTracker.DeserializationOrder.Add($"{Name}i");
+}
+
+public class BinaryTreeNodeWithEventsSurrogate : ISerializationSurrogate
+{
+ public void GetObjectData(object obj, SerializationInfo info, StreamingContext context) => throw new NotImplementedException();
+ public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector? selector)
+ {
+ BinaryTreeNodeWithEvents node = (BinaryTreeNodeWithEvents)obj;
+ node.Name = info.GetString("k__BackingField")!;
+ node.Left = (BinaryTreeNodeWithEvents)info.GetValue("k__BackingField", typeof(BinaryTreeNodeWithEvents))!;
+ node.Right = (BinaryTreeNodeWithEvents)info.GetValue("k__BackingField", typeof(BinaryTreeNodeWithEvents))!;
+ node.Value = (ValueTypeBase)info.GetValue("k__BackingField", typeof(ValueTypeBase))!;
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Add($"{node.Name}s");
+
+ return node;
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/BinaryTreeNodeWithEventsBase.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/BinaryTreeNodeWithEventsBase.cs
new file mode 100644
index 0000000000000..cfc66b77679cd
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/BinaryTreeNodeWithEventsBase.cs
@@ -0,0 +1,6 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Resources.Extensions.Tests.Common.TestTypes;
+
+public interface BinaryTreeNodeWithEventsBase { }
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/BinaryTreeNodeWithEventsISerializable.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/BinaryTreeNodeWithEventsISerializable.cs
new file mode 100644
index 0000000000000..9460b5f90c58f
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/BinaryTreeNodeWithEventsISerializable.cs
@@ -0,0 +1,44 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.Tests.Common.TestTypes;
+
+[Serializable]
+public class BinaryTreeNodeWithEventsISerializable : ISerializable, IDeserializationCallback, BinaryTreeNodeWithEventsBase
+{
+ public string Name { get; set; } = string.Empty;
+ public BinaryTreeNodeWithEventsISerializable? Left { get; set; }
+ public BinaryTreeNodeWithEventsISerializable? Right { get; set; }
+ public ValueTypeBase? Value { get; set; }
+
+ public BinaryTreeNodeWithEventsISerializable() { }
+
+ protected BinaryTreeNodeWithEventsISerializable(SerializationInfo serializationInfo, StreamingContext streamingContext)
+ {
+ Name = serializationInfo.GetString(nameof(Name))!;
+ Left = (BinaryTreeNodeWithEventsISerializable?)serializationInfo.GetValue(nameof(Left), typeof(BinaryTreeNodeWithEventsISerializable));
+ Right = (BinaryTreeNodeWithEventsISerializable?)serializationInfo.GetValue(nameof(Right), typeof(BinaryTreeNodeWithEventsISerializable));
+ Value = (ValueTypeBase?)serializationInfo.GetValue(nameof(Value), typeof(ValueTypeBase));
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Add($"{Name}s");
+ }
+
+ [OnDeserialized]
+ private void OnDeserialized(StreamingContext context) => BinaryTreeNodeWithEventsTracker.DeserializationOrder.Add($"{Name}p");
+
+ public void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ info.AddValue(nameof(Name), Name);
+ info.AddValue(nameof(Left), Left);
+ info.AddValue(nameof(Right), Right);
+ info.AddValue(nameof(Value), Value);
+ }
+
+ public void OnDeserialization(object? sender) => BinaryTreeNodeWithEventsTracker.DeserializationOrder.Add($"{Name}i");
+}
+
+public class BinaryTreeNodeWithEventsTracker
+{
+ public static List DeserializationOrder { get; } = new();
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/ClassWithValueISerializable.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/ClassWithValueISerializable.cs
new file mode 100644
index 0000000000000..52823825c1167
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/ClassWithValueISerializable.cs
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.Tests.Common.TestTypes;
+
+[Serializable]
+public class ClassWithValueISerializable : ISerializable
+{
+ public T? Value { get; set; }
+
+ public ClassWithValueISerializable() { }
+
+ protected ClassWithValueISerializable(SerializationInfo info, StreamingContext context)
+ {
+ Value = (T)info.GetValue(nameof(Value), typeof(T))!;
+ }
+
+ public void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ info.AddValue(nameof(Value), Value);
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/DerivedISerializableWithNonPublicDeserializationCtor.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/DerivedISerializableWithNonPublicDeserializationCtor.cs
new file mode 100644
index 0000000000000..08deab28e99f5
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/DerivedISerializableWithNonPublicDeserializationCtor.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.Tests.Common.TestTypes;
+
+[Serializable]
+public sealed class DerivedISerializableWithNonPublicDeserializationCtor : BasicISerializableObject
+{
+ public DerivedISerializableWithNonPublicDeserializationCtor(int value1, string value2) : base(value1, value2) { }
+ private DerivedISerializableWithNonPublicDeserializationCtor(SerializationInfo info, StreamingContext context) : base(info, context) { }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/NodeStruct.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/NodeStruct.cs
new file mode 100644
index 0000000000000..950dd5ff2e64b
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/NodeStruct.cs
@@ -0,0 +1,58 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.Tests.Common.TestTypes;
+
+[Serializable]
+public struct StructWithObject
+{
+ public object? Value;
+}
+
+[Serializable]
+public struct StructWithTwoObjects
+{
+ public object? Value;
+ public object? Value2;
+}
+
+[Serializable]
+public struct StructWithTwoObjectsISerializable : ISerializable
+{
+ public object? Value;
+ public object? Value2;
+
+ public StructWithTwoObjectsISerializable() { }
+
+ private StructWithTwoObjectsISerializable(SerializationInfo info, StreamingContext context)
+ {
+ Value = info.GetValue(nameof(Value), typeof(object));
+ Value2 = info.GetValue(nameof(Value2), typeof(object));
+ }
+
+ public readonly void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ info.AddValue(nameof(Value), typeof(object));
+ info.AddValue(nameof(Value2), typeof(object));
+ }
+}
+
+[Serializable]
+public struct NodeStruct : ISerializable
+{
+ public NodeStruct() { }
+
+ private NodeStruct(SerializationInfo info, StreamingContext context)
+ {
+ Node = (NodeWithNodeStruct)info.GetValue(nameof(Node), typeof(NodeWithNodeStruct))!;
+ }
+
+ public NodeWithNodeStruct? Node { get; set; }
+
+ public readonly void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ info.AddValue(nameof(Node), Node, typeof(NodeWithNodeStruct));
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/NodeWithNodeStruct.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/NodeWithNodeStruct.cs
new file mode 100644
index 0000000000000..526b9502cd510
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/NodeWithNodeStruct.cs
@@ -0,0 +1,11 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Resources.Extensions.Tests.Common.TestTypes;
+
+[Serializable]
+public class NodeWithNodeStruct
+{
+ public string? Value { get; set; }
+ public NodeStruct NodeStruct { get; set; }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/NodeWithValueISerializable.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/NodeWithValueISerializable.cs
new file mode 100644
index 0000000000000..f430d2277ceeb
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/NodeWithValueISerializable.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.Tests.Common.TestTypes;
+
+[Serializable]
+public class NodeWithValueISerializable : ISerializable
+{
+ public NodeWithValueISerializable() { }
+
+ protected NodeWithValueISerializable(SerializationInfo info, StreamingContext context)
+ {
+ Node = (NodeWithValueISerializable?)info.GetValue(nameof(Node), typeof(NodeWithValueISerializable));
+ Value = info.GetInt32(nameof(Value));
+ }
+
+ public NodeWithValueISerializable? Node { get; set; }
+ public int Value { get; set; }
+
+ public void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ info.AddValue(nameof(Node), Node);
+ info.AddValue(nameof(Value), Value);
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/NonSerializablePair.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/NonSerializablePair.cs
new file mode 100644
index 0000000000000..dc8ddce1640dc
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/NonSerializablePair.cs
@@ -0,0 +1,10 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Resources.Extensions.Tests.Common.TestTypes;
+
+internal sealed class NonSerializablePair
+{
+ public T1? Value1;
+ public T2? Value2;
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/NonSerializablePairSurrogate.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/NonSerializablePairSurrogate.cs
new file mode 100644
index 0000000000000..cd64ea404ad9f
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/NonSerializablePairSurrogate.cs
@@ -0,0 +1,24 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.Tests.Common.TestTypes;
+
+internal sealed class NonSerializablePairSurrogate : ISerializationSurrogate
+{
+ public void GetObjectData(object obj, SerializationInfo info, StreamingContext context)
+ {
+ var pair = (NonSerializablePair)obj;
+ info.AddValue("Value1", pair.Value1);
+ info.AddValue("Value2", pair.Value2);
+ }
+
+ public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, ISurrogateSelector? selector)
+ {
+ var pair = (NonSerializablePair)obj;
+ pair.Value1 = info.GetInt32("Value1");
+ pair.Value2 = info.GetString("Value2")!;
+ return pair;
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/SimpleNode.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/SimpleNode.cs
new file mode 100644
index 0000000000000..7891e901e2c9f
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/SimpleNode.cs
@@ -0,0 +1,10 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.Resources.Extensions.Tests.Common.TestTypes;
+
+[Serializable]
+public class SimpleNode
+{
+ public SimpleNode? Next { get; set; }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/StructThatImplementsIDeserializationCallback.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/StructThatImplementsIDeserializationCallback.cs
new file mode 100644
index 0000000000000..d83a147903ed4
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/StructThatImplementsIDeserializationCallback.cs
@@ -0,0 +1,23 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.Tests.Common.TestTypes;
+
+[Serializable]
+public struct StructThatImplementsIDeserializationCallback : ValueTypeBase
+{
+ public StructThatImplementsIDeserializationCallback()
+ {
+ }
+
+ public string Name { get; set; } = string.Empty;
+
+ public BinaryTreeNodeWithEventsBase? Reference { get; set; }
+
+ public readonly void OnDeserialization(object? sender) => BinaryTreeNodeWithEventsTracker.DeserializationOrder.Add($"{Name}i");
+
+ [OnDeserialized]
+ private readonly void OnDeserialized(StreamingContext context) => BinaryTreeNodeWithEventsTracker.DeserializationOrder.Add($"{Name}p");
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/StructWithArrayReferenceISerializable.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/StructWithArrayReferenceISerializable.cs
new file mode 100644
index 0000000000000..80789aa0741d2
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/StructWithArrayReferenceISerializable.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.Tests.Common.TestTypes;
+
+[Serializable]
+public struct StructWithArrayReferenceISerializable : ISerializable
+ where T : class
+{
+ public nint Value { get; set; }
+ public T[]? Reference { get; set; }
+
+ private StructWithArrayReferenceISerializable(SerializationInfo info, StreamingContext context)
+ {
+ Reference = (T[]?)info.GetValue(nameof(Reference), typeof(T));
+ Value = (nint)info.GetValue(nameof(Value), typeof(nint))!;
+ }
+
+ public readonly void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ info.AddValue(nameof(Reference), Reference);
+ info.AddValue(nameof(Value), Value, typeof(nint));
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/StructWithReferenceISerializable.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/StructWithReferenceISerializable.cs
new file mode 100644
index 0000000000000..c313ddaad1174
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/StructWithReferenceISerializable.cs
@@ -0,0 +1,26 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.Tests.Common.TestTypes;
+
+[Serializable]
+public struct StructWithReferenceISerializable : ISerializable
+ where T : class
+{
+ public nint Value { get; set; }
+ public T? Reference { get; set; }
+
+ private StructWithReferenceISerializable(SerializationInfo info, StreamingContext context)
+ {
+ Reference = (T?)info.GetValue(nameof(Reference), typeof(T));
+ Value = (nint)info.GetValue(nameof(Value), typeof(nint))!;
+ }
+
+ public readonly void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ info.AddValue(nameof(Reference), Reference);
+ info.AddValue(nameof(Value), Value, typeof(nint));
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/StructWithSelfArrayReferenceISerializable.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/StructWithSelfArrayReferenceISerializable.cs
new file mode 100644
index 0000000000000..2277c6c2aef57
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/StructWithSelfArrayReferenceISerializable.cs
@@ -0,0 +1,27 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.Tests.Common.TestTypes;
+
+[Serializable]
+public struct StructWithSelfArrayReferenceISerializable : ISerializable
+{
+ public nint Value { get; set; }
+ public StructWithSelfArrayReferenceISerializable[]? Array { get; set; }
+
+ public StructWithSelfArrayReferenceISerializable() { }
+
+ private StructWithSelfArrayReferenceISerializable(SerializationInfo info, StreamingContext context)
+ {
+ Array = (StructWithSelfArrayReferenceISerializable[]?)info.GetValue(nameof(Array), typeof(StructWithSelfArrayReferenceISerializable[]))!;
+ Value = (nint)info.GetValue(nameof(Value), typeof(nint))!;
+ }
+
+ public readonly void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ info.AddValue(nameof(Array), Array);
+ info.AddValue(nameof(Value), Value, typeof(nint));
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/ValueTypeBase.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/ValueTypeBase.cs
new file mode 100644
index 0000000000000..6ab295045cdf6
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/ValueTypeBase.cs
@@ -0,0 +1,13 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.Tests.Common.TestTypes;
+
+public interface ValueTypeBase : IDeserializationCallback
+{
+ public string Name { get; set; }
+
+ public BinaryTreeNodeWithEventsBase? Reference { get; set; }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/ValueTypeISerializable.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/ValueTypeISerializable.cs
new file mode 100644
index 0000000000000..12d2c0c593c0f
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/Common/TestTypes/ValueTypeISerializable.cs
@@ -0,0 +1,31 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization;
+
+namespace System.Resources.Extensions.Tests.Common.TestTypes;
+
+[Serializable]
+public struct ValueTypeISerializable : ISerializable, ValueTypeBase
+{
+ public string Name { get; set; } = string.Empty;
+
+ public BinaryTreeNodeWithEventsBase? Reference { get; set; }
+
+ private ValueTypeISerializable(SerializationInfo serializationInfo, StreamingContext streamingContext)
+ {
+ Name = serializationInfo.GetString(nameof(Name))!;
+ Reference = (BinaryTreeNodeWithEventsISerializable?)serializationInfo.GetValue(nameof(Reference), typeof(BinaryTreeNodeWithEventsISerializable));
+ BinaryTreeNodeWithEventsTracker.DeserializationOrder.Add($"{Name}s");
+ }
+
+ public readonly void GetObjectData(SerializationInfo info, StreamingContext context)
+ {
+ info.AddValue(nameof(Name), Name);
+ info.AddValue(nameof(Reference), Reference);
+ }
+
+ [OnDeserialized]
+ private readonly void OnDeserialized(StreamingContext context) => BinaryTreeNodeWithEventsTracker.DeserializationOrder.Add($"{Name}p");
+ public readonly void OnDeserialization(object? sender) => BinaryTreeNodeWithEventsTracker.DeserializationOrder.Add($"{Name}i");
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/FormattedObject/ArrayTests.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/FormattedObject/ArrayTests.cs
new file mode 100644
index 0000000000000..9b01702ce14b3
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/FormattedObject/ArrayTests.cs
@@ -0,0 +1,59 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Drawing;
+using System.Linq;
+using System.Resources.Extensions.BinaryFormat;
+using System.Runtime.Serialization.BinaryFormat;
+
+namespace System.Resources.Extensions.Tests.FormattedObject;
+
+public class ArrayTests : Common.ArrayTests
+{
+ public override void Roundtrip_ArrayContainingArrayAtNonZeroLowerBound()
+ {
+ Action action = base.Roundtrip_ArrayContainingArrayAtNonZeroLowerBound;
+ action.Should().Throw();
+ }
+
+ [Theory]
+ [MemberData(nameof(StringArray_Parse_Data))]
+ public void StringArray_Parse(string?[] strings)
+ {
+ BinaryFormattedObject format = new(Serialize(strings));
+ var arrayRecord = (ArrayRecord)format.RootRecord;
+ arrayRecord.GetArray().Should().BeEquivalentTo(strings);
+ }
+
+ public static TheoryData StringArray_Parse_Data => new()
+ {
+ new string?[] { "one", "two" },
+ new string?[] { "yes", "no", null },
+ new string?[] { "same", "same", "same" }
+ };
+
+ [Theory]
+ [MemberData(nameof(PrimitiveArray_Parse_Data))]
+ public void PrimitiveArray_Parse(Array array)
+ {
+ BinaryFormattedObject format = new(Serialize(array));
+ var arrayRecord = (ArrayRecord)format.RootRecord;
+ arrayRecord.GetArray(expectedArrayType: array.GetType()).Should().BeEquivalentTo(array);
+ }
+
+ public static TheoryData PrimitiveArray_Parse_Data => new()
+ {
+ new int[] { 1, 2, 3 },
+ new int[] { 1, 2, 1 },
+ new float[] { 1.0f, float.NaN, float.PositiveInfinity },
+ new DateTime[] { DateTime.MaxValue }
+ };
+
+ public static IEnumerable Array_TestData => StringArray_Parse_Data.Concat(PrimitiveArray_Parse_Data);
+
+ public static TheoryData Array_UnsupportedTestData => new()
+ {
+ new Point[] { new() },
+ new object[] { new() },
+ };
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/FormattedObject/BasicObjectTests.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/FormattedObject/BasicObjectTests.cs
new file mode 100644
index 0000000000000..633e175a6e099
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/FormattedObject/BasicObjectTests.cs
@@ -0,0 +1,42 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.Serialization.Formatters.Binary;
+using BinaryFormatTests;
+
+namespace System.Resources.Extensions.Tests.FormattedObject;
+
+public class BasicObjectTests : Common.BasicObjectTests
+{
+ private protected override bool SkipOffsetArrays => true;
+
+ [Theory]
+ [MemberData(nameof(SerializableObjects))]
+ public void BasicObjectsRoundTripAndMatch(object value, TypeSerializableValue[] _)
+ {
+ // We need to round trip through the BinaryFormatter as a few objects in tests remove
+ // serialized data on deserialization.
+ BinaryFormatter formatter = new();
+ MemoryStream serialized = new();
+ formatter.Serialize(serialized, value);
+ serialized.Position = 0;
+ object bfdeserialized = formatter.Deserialize(serialized);
+ serialized.Position = 0;
+ serialized.SetLength(0);
+ formatter.Serialize(serialized, bfdeserialized);
+ serialized.Position = 0;
+
+ // Now deserialize with BinaryFormattedObject
+ object deserialized = Deserialize(serialized);
+
+ // And reserialize what we serialized with the BinaryFormatter
+ MemoryStream deserializedSerialized = new();
+ formatter.Serialize(deserializedSerialized, deserialized);
+
+ deserializedSerialized.Position = 0;
+ serialized.Position = 0;
+
+ // Now compare the two streams to ensure they are identical
+ deserializedSerialized.Length.Should().Be(serialized.Length);
+ }
+}
diff --git a/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/FormattedObject/BinaryFormattedObjectTests.cs b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/FormattedObject/BinaryFormattedObjectTests.cs
new file mode 100644
index 0000000000000..34f5261cfec11
--- /dev/null
+++ b/src/libraries/System.Resources.Extensions/tests/BinaryFormatTests/FormattedObject/BinaryFormattedObjectTests.cs
@@ -0,0 +1,329 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections;
+using System.Linq;
+using System.Resources.Extensions.BinaryFormat;
+using System.Runtime.Serialization.BinaryFormat;
+using System.Resources.Extensions.Tests.Common;
+
+namespace System.Resources.Extensions.Tests.FormattedObject;
+
+public class BinaryFormattedObjectTests : SerializationTest
+{
+ [Fact]
+ public void ReadHeader()
+ {
+ BinaryFormattedObject format = new(Serialize("Hello World."));
+ format.RootRecord.ObjectId.Should().Be(1);
+ }
+
+ [Theory]
+ [InlineData("Hello there.")]
+ [InlineData("")]
+ [InlineData("Embedded\0 Null.")]
+ public void ReadBinaryObjectString(string testString)
+ {
+ BinaryFormattedObject format = new(Serialize(testString));
+ PrimitiveTypeRecord stringRecord = (PrimitiveTypeRecord