Stream-Serializer-Extensions
1.6.0
See the version list below for details.
dotnet add package Stream-Serializer-Extensions --version 1.6.0
NuGet\Install-Package Stream-Serializer-Extensions -Version 1.6.0
<PackageReference Include="Stream-Serializer-Extensions" Version="1.6.0" />
paket add Stream-Serializer-Extensions --version 1.6.0
#r "nuget: Stream-Serializer-Extensions, 1.6.0"
// Install Stream-Serializer-Extensions as a Cake Addin #addin nuget:?package=Stream-Serializer-Extensions&version=1.6.0 // Install Stream-Serializer-Extensions as a Cake Tool #tool nuget:?package=Stream-Serializer-Extensions&version=1.6.0
Stream-Serializer-Extensions
This .NET library extends any Stream
object with serializing methods for
writing almost any object binary serialized to a stream, and deserializing any
binary stream sequence.
The built in serializer supports binary serialization of
- booleans
- numbers ((U)Int8-64, Single, Double, Decimal)
- enumerations
- strings (UTF-8)
- arrays
- lists
- dictionaries
- byte arrays
- possibly any other objects with a parameterless public constructor
NOTE: Arrays, lists and dictionaries with nullable values aren't supported.
It's possible to override the build in serializers, and to add custom type serializers, too.
Methods
The Write
and WriteAsync
methods will be extended with supported types,
while serializing some types is being done with specialized methods:
Type | Serialization method | Deserialization method |
---|---|---|
string |
WriteString* |
ReadString* |
Enum |
WriteEnum* |
ReadEnum* |
Array |
WriteArray* |
ReadArray* |
List<T> |
WriteList* |
ReadList* |
Dictionary<tKey, tValue> |
WriteDict* |
ReadDict* |
IStreamSerializer |
WriteSerialized* |
ReadSerialized* |
byte[] |
WriteBytes* |
ReadBytes* |
(any other) | WriteAnyObject* |
ReadAnyObject* |
Using the WriteObject*
and ReadObject*
methods you can let the library
decide which method to use for the given object type.
Using the WriteAny*
and ReadAny*
methods, you can write and read an object
with a dynamic type.
Using the WriteAnyObject*
and ReadAnyObject*
methods you can also
(de)serialize objects which have a constructor without parameters. The
serializer will process public properties which have a public getter and
setter. If in the future the object changes, it's not possible to deserialize
an older binary sequence, unless you work with the StreamSerializerAttribute
and set an object version to the type and its properties.
NOTE: Please use the *Nullable*
methods for working with nullables.
They'll add an extra byte for null
value detection.
NOTE: In general you should use the opposite method for reading a binary sequence that you've used for writing it!
Most methods are designed specially for one type, while other methods work more generic. This is when to choose which method:
Method | Condition |
---|---|
*Serialized* |
The fixed type is a stream serializer base object |
*Object* |
The fixed type has a specialized serializer |
*AnyObject* |
The type uses attributes (or no serializer contract information at all) and doesn't have a specialized serializer |
*Any* |
The dynamic type is unknown when (de)serializing |
Number serialization
The WriteNumber*
and ReadNumber*
methods will find the best matching
serialization method for a given number. For example, if you give an Int32
value which could be fit into an UInt16, the value will be converted for
serialization, and you can save one byte (because the methods will store the
used numeric type in an extra byte).
NOTE: All numbers will be serialized using little endian.
Custom serializer
Using the StreamSerializerAttribute
attribute
The type needs to have a constructor without parameters. Properties with a public getter and setter can be serialized:
[StreamSerializer(StreamSerializerModes.OptOut)]
public class YourType
{
public YourType() { }
public string Serialized { get; set; } = null!;
[StreamSerializer]
public string? NotSerialized { get; set; }
}
stream.WriteAnyObject(new YourType(){ Serialized = "Test1", NotSerialized = "Test2" });
stream.Position = 0;
YourType deserialized = stream.ReadAnyObject<YourType>();
Assert.AreEqual(deserialized.Serialized, "Test1");
Assert.IsNull(deserialized.NotSerialized);
Using the StreamSerializerAttribute
attribute at a type, you can set the
serializer mode to OptOut
(the default when not using the attribute) or
OptIn
. OptOut
includes all properties, except the ones with a
StreamSerializerAttribute
(if the attribute doesn't OptIn
). OptIn
only
includes properties with a StreamSerializerAttribute
, except the attribute
mode was set to OptOut
.
To skip property name checksums, which require one extra byte per serialized
property, you can set the SkipPropertyNameChecksum
property value of the
attribute to true
.
You can use a property versioning, which supports skipping newer properties when deserializing an older binary sequence. The type attribute defines the object version, while the property attributes define the object version in which they (dis)appear.
By setting a property value position, you can modify the order of the value within the binary sequence. This ordering will be applied:
Position
Name
To exclude a property depending on the object version:
FromVersion
: First object version which includes the property (optional)Version
: Last object version which includes the property (optional)
Extending the StreamSerializer
StreamSerializer.SyncSerializer.AddOrUpdate(
typeof(YourType),
(stream, value) =>
{
// Serialize value to stream
}
);
StreamSerializer.AsyncSerializer.AddOrUpdate(
typeof(YourType),
async (stream, value, cancellationToken) =>
{
// Serialize value to stream
}
);
StreamSerializer.SyncDeserializer.AddOrUpdate(
typeof(YourType),
(stream, type, version) =>
{
// Deserialize value from stream
return value;
}
);
StreamSerializer.AsyncDeserializer.AddOrUpdate(
typeof(YourType),
YourType.DeserializeAsync
);
public class YourType
{
...
public static async Task<YourType> DeserializeAsync(Stream stream, Type type, int version, CancellationToken cancellationToken)
{
// Deserialize value
return value;
}
}
// Then you can (de)serialize like this:
stream.WriteObject(new YourType());
stream.Position = 0;
YourType instance = stream.ReadObject<YourType>();
NOTE: The asynchronous deserializer delegate uses a Task
return type,
while internal the task will be converted to Task<YourType>
, to get the
result. Because there's a lack of support for generic delegates, this seems to
be the only way to go 😦
NOTE: You can attach to the StreamSerializer.OnInit
event to add your
custom type serializers on start. During initialization, the
StreamSerializer.SyncObject
object will be thread locked, and you shouldn't
use the Find*
methods.
NOTE: You can use the SerializerHelper
class methods for boiler plate
tasks like ensuring a non-null value, or validating a deserialized length
value, for example.
NOTE: You should throw a SerializerException
on any (de)serializing
issue!
Using the IStreamSerializer
interface
Your object can implement the IStreamSerializer
interface or use the
StreamSerializerBase
base class, which implements the IStreamSerializer
interface. Then you can use the WriteSerialized*
, WriteObject*
,
ReadSerialized*
and ReadObject*
methods for (de)serialization.
public class YourType : StreamSerializerBase
{
public YourType() : base() { }
public YourType(Stream stream, int version) : base(stream, version) { }
protected override void Serialize(Stream stream)
{
...
}
protected override void Deserialize(Stream stream, int version)
{
...
}
}
When deserializing using the ReadAny*
methods, the target type needs to be
loaded from the environment. You can add your own type loading handler using
the StreamSerializer.OnLoadType
event. The library uses the wan24-Core
NuGet package. If you want to use the wan24-Core
type helper for loading
types:
StreamSerializer.OnInit += (e) => StreamSerializer.OnLoadType += (s, e) =>
{
if(e.Type != null) return;
e.Type = TypeHelper.Instance.GetType(e.Name);
};
CAUTION: By adding the wan24-Core
type helper like this, any type may be
deserialized, which may be is a security issue!
When using the StreamSerializerBase
base class, you can also give a value
for the parameter objectVersion
to the base constructor to enable object
versioning. This makes it possible that newer object versions are able to
deserialize from older binary sequences, and it ensures that old object
versions can't deserialize from a newer binary sequence. During
deserialization you can get the serialized object version like this:
...
public const int OBJECT_VERSION = 1;
public YourType() : base(objectVersion: OBJECT_VERSION) { }
public YourType(Stream stream, int version) : base(stream, version, objectVersion: OBJECT_VERSION) { }
...
protected override void Deserialize(Stream stream, int version)
{
int serializedVersion = ((IStreamSerializer)this).SerializedObjectVersion!;
if(serializedVersion < 1) throw new SerializerExeption($"Unsupported {GetType()} binary sequence version #{serializedVersion}");
...
}
...
The SerializedObjectVersion
property will have a non-null value, if the
object was deserialized, and the StreamSerializerBase
base constructor got a
object version as objectVersion
parameter. Based on the serialized object
version you can switch and handle the binary sequence in the required way. To
access the versioning information of an object, you can use the optional
IStreamSerializerVersion
interface, which is implemented by
StreamSerializerBase
, too.
Deserializer limitations
When deserializing variable length objects (like arrays or strings), you can
limit the allowed number of items/bytes (or request a minimum count) using the
specific (de)serializer methods, by giving minLen
and maxLen
parameter
values.
Serializer version
The StreamSerializer.Version
property holds the serializer version
information, which you may write at the beginning of a serialized stream. The
first byte (values 0-255) is used by the stream serializer internal. If you'd
like custom versioning, please use bytes 2 to 4 (values 256+) for your own
version number (which you can make more readable by bit shifting the value).
The stream serializer extensions will only use the first 8 bits to identify a
serializer version number, which can be given to all deserializer methods,
while the StreamSerializer.Version
value is the default serializer version
number to use in case no version parameter was given to a deserializer method.
NOTE: The serializing methods will always use the latest binary sequence
format version when writing, no matter which value was set to
StreamSerializer.Version
!
Binary sequence size
Since the serializer may be used without any configuration and contract information, it tries to create the smallest binary sequence size which still offers enough information for an also unconfigured deserialization without contracts. The currently resulting sequence size is a trade off between size and usability. By defining customized type serializers you can optimize the resulting sequence size for any type. Keep in mind, that using the most specific (de)serializing methods will result in smaller binary sequences.
Stream object enumeration
You can enumerate serialized objects from any stream like this:
foreach(AnyType obj in stream.EnumerateSerialized<AnyType>())
{
...
}
await foreach(AnyType obj in stream.EnumerateSerializedAsync<AnyType>())
{
...
}
In this example it's assumed that AnyType
implements IStreamSerializer
.
For enumerating any other type, you can implement an enumerator using the
StreamEnumeratorBase
and StreamAsyncEnumeratorBase
base classes. The only
thing that you'll have to do is to override the ReadObject(Async)
method,
which finally reads the next object to yield from the Stream
. Then you can
enumerate easily using the static Enumerate(Async)
methods of the base types.
This is a sample bool
enumerator implementation, which uses the ReadBool
method:
public class StreamBoolEnumerator : StreamEnumeratorBase<bool>
{
public StreamBoolEnumerator(Stream stream, int? version = null)
:base(stream, version)
{ }
protected override int ReadObject() => Stream.ReadBool(SerializerVersion);
}
To provide the enumerator as a stream extension method:
public static IEnumerable<bool> EnumerateBool(this Stream stream, int? version = null)
=> StreamBoolEnumerator.Enumerate<StreamBoolEnumerator>(stream, version);
These enumerators are implemented at present:
Type | Enumerator | Serializer method | Stream extension |
---|---|---|---|
IStreamSerializer |
StreamSerializer(Async)Enumerator |
ReadSerialized(Async) |
EnumerateSerialized(Async) |
Numeric types | StreamNumber(Async)Enumerator |
ReadNumber(Async) |
EnumerateNumber(Async) |
string |
StreamString(Async)Enumerator |
ReadString(Async) |
EnumerateString(Async) |
Security
The base serializer supports basic types and lists. Especially when deserializing lists, you should define a minimum and a maximum length.
Per default all objects that you want to deserialize using a ReadAnyObject*
method, the type is required to use the StreamSerializerAttribute
for
security reasons. If you need to allow all types, you can set the
StreamExtensions.AnyObjectRequireAttribute
property value to false
.
CAUTION: If you allow deserialization of any type, deserializing a manipulated input stream could harm your computer!
CAUTION: During serialization it's possible to end up in an endless recursion, if any nested property serves an object which is in the current stack already.
CAUTION: If you don't use versioning, you may end up in broken binary sequences which can't be deserialized anymore. Also a deserialization attempt could harm your computer!
The job of the serializer is to write and read objects to/from a binary sequence. There's no compression, encryption or hashing built in. If you want to compress/protect a created binary sequence, you can apply compression, encryption and hashing on the result as you want.
Object validation will be applied to deserialized objects to ensure their validity.
Roadmap
- Enumerate objects from a stream
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net6.0
- ObjectValidation (>= 1.4.0)
- wan24-Core (>= 1.6.1)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Stream-Serializer-Extensions:
Package | Downloads |
---|---|
wan24-Compression
Compression helper |
|
wan24-Crypto
Crypto helper |
|
wan24-Crypto-TPM
TPM crypto helper extension package for wan24-Crypto |
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated | |
---|---|---|---|
3.14.0 | 64 | 11/10/2024 | |
3.13.0 | 223 | 10/27/2024 | |
3.11.0 | 290 | 9/21/2024 | |
3.10.0 | 224 | 9/9/2024 | |
3.9.0 | 407 | 8/16/2024 | |
3.8.1 | 422 | 7/13/2024 | |
3.8.0 | 258 | 7/6/2024 | |
3.7.0 | 649 | 5/9/2024 | |
3.6.0 | 253 | 4/14/2024 | |
3.5.0 | 387 | 3/9/2024 | |
3.4.0 | 214 | 3/2/2024 | |
3.3.0 | 261 | 2/24/2024 | |
3.2.0 | 335 | 2/10/2024 | |
3.1.0 | 221 | 1/20/2024 | |
3.0.0 | 214 | 12/17/2023 | |
2.11.3 | 317 | 11/11/2023 | |
2.11.2 | 253 | 10/29/2023 | |
2.11.1 | 280 | 10/21/2023 | |
2.11.0 | 320 | 10/15/2023 | |
2.10.0 | 306 | 10/7/2023 | |
2.9.0 | 255 | 9/27/2023 | |
2.8.0 | 391 | 9/16/2023 | |
2.7.0 | 263 | 9/10/2023 | |
2.6.0 | 270 | 9/3/2023 | |
2.5.0 | 547 | 7/22/2023 | |
2.4.0 | 192 | 6/11/2023 | |
2.3.1 | 508 | 6/3/2023 | |
2.3.0 | 307 | 6/3/2023 | |
2.2.0 | 304 | 5/29/2023 | |
2.1.0 | 323 | 5/27/2023 | |
2.0.0 | 329 | 5/20/2023 | |
1.6.0 | 464 | 5/6/2023 | |
1.5.0 | 388 | 5/1/2023 | |
1.4.1 | 464 | 4/30/2023 | |
1.4.0 | 389 | 4/29/2023 | |
1.3.1 | 666 | 4/26/2023 | |
1.3.0 | 227 | 4/25/2023 | |
1.2.0 | 441 | 4/22/2023 | |
1.1.0 | 276 | 4/16/2023 | |
1.0.0 | 211 | 4/10/2023 |