diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..c409373
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,124 @@
+# Changelog
+
+All notable changes to this project are documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+This changelog was reconstructed from the project's commit history and published
+NuGet releases; entries for older versions are a best-effort summary.
+
+## [4.2.0] - 2026-06-16
+
+### Added
+- Option to pass a custom `ISerializationSurrogateProvider` to the serializers via the new
+ `DataSerializer.SerializationSurrogateProvider` property, allowing callers to customize
+ serialization and add support for otherwise unsupported types ([#178]).
+
+### Notes
+- The surrogate provider is honored by the XML serializer only. The JSON serializer
+ (`DataContractJsonSerializer`) does not apply surrogate providers
+ ([dotnet/runtime#100553](https://github.com/dotnet/runtime/issues/100553)).
+- The property is available on the `net6.0`–`net10.0` and `netstandard2.1` targets;
+ it is not exposed on `net48`/`net481`/`netstandard2.0`, which lack
+ `ISerializationSurrogateProvider`.
+
+## [4.1.0] - 2026-06-16
+
+### Added
+- Support for .NET 10 (`net10.0`).
+
+### Changed
+- Replaced AppVeyor with GitHub Actions for CI; publishing now uses NuGet trusted
+ publishing (OIDC) instead of a stored API key.
+- Added Dependabot configuration for NuGet updates.
+- Documentation updates (README supported platforms, project guidance).
+
+## [4.0.0] - 2025-01-19
+
+### Added
+- Support for .NET 9 (`net9.0`).
+
+## [3.1.0] - 2023-10-21
+
+### Added
+- Support for .NET 8 (`net8.0`) and .NET Framework 4.8.1 (`net481`).
+- `DateOnly` and `TimeOnly` added to the known types.
+
+### Fixed
+- Serialization of unbound `ToString` expressions ([#169]).
+
+### Removed
+- Dropped .NET 5 target (out of support).
+
+## [3.0.0] - 2023-06-10
+
+### Added
+- Support for .NET 7 (`net7.0`).
+- `netstandard2.0` and `netstandard2.1` targets.
+- README is now included in the NuGet package.
+
+### Changed
+- Suppressed `SYSLIB0011` warnings during the transition away from `BinaryFormatter`.
+
+### Fixed
+- Do not explode interface types as nullable types ([#163]).
+
+### Removed
+- Removed `BinaryFormatSerializer` / `BinaryFormatter`-based serialization due to
+ security concerns.
+
+## [2.0.0] - 2020-12-12
+
+### Added
+- Support for .NET 5 (`net5.0`).
+- Symbol package (`.snupkg`) support.
+
+### Changed
+- License changed to MIT; removed legacy copyright/license headers.
+- Added support for `DefaultExpression` ([#146]).
+- `DateTimeOffset` added to the known types ([#107]).
+
+### Removed
+- Removed deprecated target frameworks and example projects.
+
+## [1.8.1] - 2019-04-25
+
+### Fixed
+- Follow-up fix for finding the real constant value ([#113]).
+- Support for `IndexExpression` serialization ([#119]).
+
+## [1.8.0] - 2019-04-25
+
+### Fixed
+- Try harder to find the real constant value ([#113]).
+
+## [1.7.3] - 2018-10-26
+
+### Fixed
+- Fix for member access on constant values ([#105]).
+- Removed obsolete Silverlight/Windows Phone compiler switches.
+
+## [1.7.2] - 2018-10-26
+
+### Added
+- `DateTimeOffset` added as a known type ([#107]).
+
+## [1.7.1] - 2018-06-02
+
+### Fixed
+- Updated `Microsoft.Data.OData` reference (security alert).
+
+## [1.7.0] - 2018-03-20
+
+- Baseline release.
+
+[Unreleased]: https://github.com/esskar/Serialize.Linq/compare/main...HEAD
+[#178]: https://github.com/esskar/Serialize.Linq/issues/178
+[#169]: https://github.com/esskar/Serialize.Linq/issues/169
+[#163]: https://github.com/esskar/Serialize.Linq/pull/163
+[#146]: https://github.com/esskar/Serialize.Linq/issues/146
+[#119]: https://github.com/esskar/Serialize.Linq/issues/119
+[#113]: https://github.com/esskar/Serialize.Linq/issues/113
+[#107]: https://github.com/esskar/Serialize.Linq/issues/107
+[#105]: https://github.com/esskar/Serialize.Linq/issues/105
diff --git a/src/Serialize.Linq.Tests/Issues/Issue178.cs b/src/Serialize.Linq.Tests/Issues/Issue178.cs
new file mode 100644
index 0000000..cb6f90a
--- /dev/null
+++ b/src/Serialize.Linq.Tests/Issues/Issue178.cs
@@ -0,0 +1,76 @@
+using System;
+using System.Linq.Expressions;
+using System.Runtime.Serialization;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Serialize.Linq.Serializers;
+
+namespace Serialize.Linq.Tests.Issues
+{
+ ///
+ /// https://github.com/esskar/Serialize.Linq/issues/178
+ /// Adds the ability to pass an to the underlying
+ /// data contract serializer so callers can customize serialization / add support for unknown types.
+ ///
+ /// Note: the JSON serializer does not honor the surrogate provider on all runtimes
+ /// (see https://github.com/dotnet/runtime/issues/100553), so these tests exercise the XML serializer.
+ ///
+ [TestClass]
+ public class Issue178
+ {
+ ///
+ /// A surrogate provider that does not substitute anything but records that it was queried,
+ /// proving the provider is actually wired into the data contract serializer.
+ ///
+ private sealed class RecordingSurrogateProvider : ISerializationSurrogateProvider
+ {
+ public int GetSurrogateTypeCalls { get; private set; }
+
+ public Type GetSurrogateType(Type type)
+ {
+ GetSurrogateTypeCalls++;
+ return type;
+ }
+
+ public object GetObjectToSerialize(object obj, Type targetType) => obj;
+
+ public object GetDeserializedObject(object obj, Type targetType) => obj;
+ }
+
+ [TestMethod]
+ public void SurrogateProviderIsInvokedDuringXmlRoundTrip()
+ {
+ var provider = new RecordingSurrogateProvider();
+ var xmlSerializer = new XmlSerializer
+ {
+ SerializationSurrogateProvider = provider
+ };
+ var serializer = new ExpressionSerializer(xmlSerializer);
+
+ Expression> expression = x => x > 5;
+
+ var text = serializer.SerializeText(expression);
+ var actual = (Expression>)serializer.DeserializeText(text);
+
+ Assert.IsTrue(provider.GetSurrogateTypeCalls > 0, "surrogate provider was not invoked.");
+
+ var func = actual.Compile();
+ Assert.IsTrue(func(6));
+ Assert.IsFalse(func(4));
+ }
+
+ [TestMethod]
+ public void NoSurrogateProviderStillRoundTrips()
+ {
+ var serializer = new ExpressionSerializer(new XmlSerializer());
+
+ Expression> expression = x => x > 5;
+
+ var text = serializer.SerializeText(expression);
+ var actual = (Expression>)serializer.DeserializeText(text);
+
+ var func = actual.Compile();
+ Assert.IsTrue(func(6));
+ Assert.IsFalse(func(4));
+ }
+ }
+}
diff --git a/src/Serialize.Linq/Serialize.Linq.csproj b/src/Serialize.Linq/Serialize.Linq.csproj
index 1d0f076..5361524 100644
--- a/src/Serialize.Linq/Serialize.Linq.csproj
+++ b/src/Serialize.Linq/Serialize.Linq.csproj
@@ -29,9 +29,9 @@
net48;net481;net6.0;net7.0;net8.0;net9.0;net10.0;netstandard2.0;netstandard2.1
- 4.1.0
- 4.1.0.0
- 4.1.0.0
+ 4.2.0
+ 4.2.0.0
+ 4.2.0.0
LICENSE
diff --git a/src/Serialize.Linq/Serializers/DataSerializer.cs b/src/Serialize.Linq/Serializers/DataSerializer.cs
index e4d1de2..1766eda 100644
--- a/src/Serialize.Linq/Serializers/DataSerializer.cs
+++ b/src/Serialize.Linq/Serializers/DataSerializer.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.IO;
using Serialize.Linq.Nodes;
using System.Runtime.Serialization;
@@ -8,12 +8,29 @@ namespace Serialize.Linq.Serializers
{
public abstract class DataSerializer : SerializerBase, ISerializer
{
+#if NETSTANDARD2_1 || NET6_0_OR_GREATER
+ ///
+ /// Gets or sets a custom that is attached to the
+ /// underlying data contract serializer. A surrogate provider lets callers substitute types during
+ /// serialization and deserialization, for example to add support for types that the data contract
+ /// serializer cannot handle on its own.
+ ///
+ ///
+ /// This property is not available on the .NET Framework / netstandard2.0 targets, which do not
+ /// expose .
+ /// Note that the JSON serializer does not honor the surrogate provider on all runtimes
+ /// (see https://github.com/dotnet/runtime/issues/100553); prefer the XML serializer when a
+ /// surrogate provider is required.
+ ///
+ public ISerializationSurrogateProvider SerializationSurrogateProvider { get; set; }
+#endif
+
public virtual void Serialize(Stream stream, T obj) where T : Node
{
if (stream == null)
throw new ArgumentNullException(nameof(stream));
- var serializer = CreateXmlObjectSerializer(typeof(T));
+ var serializer = CreateSerializer(typeof(T));
serializer.WriteObject(stream, obj);
}
@@ -22,10 +39,22 @@ public virtual T Deserialize(Stream stream) where T : Node
if (stream == null)
throw new ArgumentNullException(nameof(stream));
- var serializer = CreateXmlObjectSerializer(typeof(T));
+ var serializer = CreateSerializer(typeof(T));
return (T)serializer.ReadObject(stream);
}
+ private XmlObjectSerializer CreateSerializer(Type type)
+ {
+ var serializer = CreateXmlObjectSerializer(type);
+#if NETSTANDARD2_1 || NET6_0_OR_GREATER
+ // SetSerializationSurrogateProvider is only available on DataContractSerializer (XML).
+ // DataContractJsonSerializer does not honor a surrogate provider (dotnet/runtime#100553).
+ if (SerializationSurrogateProvider != null && serializer is DataContractSerializer dataContractSerializer)
+ dataContractSerializer.SetSerializationSurrogateProvider(SerializationSurrogateProvider);
+#endif
+ return serializer;
+ }
+
protected abstract XmlObjectSerializer CreateXmlObjectSerializer(Type type);
}
}