Skip to content

Commit

Permalink
Feat: Implement MessagePack serialization with binary optimization
Browse files Browse the repository at this point in the history
  • Loading branch information
Taiizor committed Jan 8, 2025
1 parent 5628420 commit 95b9c13
Show file tree
Hide file tree
Showing 14 changed files with 788 additions and 2 deletions.
21 changes: 21 additions & 0 deletions UUID.sln
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UUID.Serialization.System.B
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UUID.Compare.Benchmarks", "benchmark\UUID.Compare.Benchmarks\UUID.Compare.Benchmarks.csproj", "{7836A9A9-ABF1-F1F9-DF37-F3F7115C4048}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UUID.Serialization.MessagePack", "src\UUID.Serialization.MessagePack\UUID.Serialization.MessagePack.csproj", "{0768C1B3-C996-57D2-0C66-397FF5ED9D99}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UUID.Serialization.MessagePack.Tests", "test\UUID.Serialization.MessagePack.Tests\UUID.Serialization.MessagePack.Tests.csproj", "{35FBB63B-1D89-473C-71AC-E93966A0CAEE}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UUID.Serialization.MessagePack.Benchmarks", "benchmark\UUID.Serialization.MessagePack.Benchmarks\UUID.Serialization.MessagePack.Benchmarks.csproj", "{88F5B9BD-EC39-8329-FA2E-B48A9C8A9E6E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -101,6 +107,18 @@ Global
{7836A9A9-ABF1-F1F9-DF37-F3F7115C4048}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7836A9A9-ABF1-F1F9-DF37-F3F7115C4048}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7836A9A9-ABF1-F1F9-DF37-F3F7115C4048}.Release|Any CPU.Build.0 = Release|Any CPU
{0768C1B3-C996-57D2-0C66-397FF5ED9D99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0768C1B3-C996-57D2-0C66-397FF5ED9D99}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0768C1B3-C996-57D2-0C66-397FF5ED9D99}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0768C1B3-C996-57D2-0C66-397FF5ED9D99}.Release|Any CPU.Build.0 = Release|Any CPU
{35FBB63B-1D89-473C-71AC-E93966A0CAEE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{35FBB63B-1D89-473C-71AC-E93966A0CAEE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{35FBB63B-1D89-473C-71AC-E93966A0CAEE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{35FBB63B-1D89-473C-71AC-E93966A0CAEE}.Release|Any CPU.Build.0 = Release|Any CPU
{88F5B9BD-EC39-8329-FA2E-B48A9C8A9E6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{88F5B9BD-EC39-8329-FA2E-B48A9C8A9E6E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{88F5B9BD-EC39-8329-FA2E-B48A9C8A9E6E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{88F5B9BD-EC39-8329-FA2E-B48A9C8A9E6E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -119,6 +137,9 @@ Global
{B3458EF5-6BDF-A94E-1E6B-B9FD6FF363BD} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{8F0B6B6A-7336-DE7A-5B79-AD1C38F60139} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{7836A9A9-ABF1-F1F9-DF37-F3F7115C4048} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
{0768C1B3-C996-57D2-0C66-397FF5ED9D99} = {A98F348B-49A9-43CB-924A-9F0A40CA88B2}
{35FBB63B-1D89-473C-71AC-E93966A0CAEE} = {02D54F9E-8518-40C9-B2CD-2A4F8CA339A3}
{88F5B9BD-EC39-8329-FA2E-B48A9C8A9E6E} = {02EA681E-C7D8-13C7-8484-4AC65E1B71E8}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {5DA4A105-4BC7-4943-9328-F7274E75FCB3}
Expand Down
12 changes: 12 additions & 0 deletions benchmark/UUID.Serialization.MessagePack.Benchmarks/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using BenchmarkDotNet.Running;

namespace UUIDSerializationMessagePackBenchmarks
{
internal class Program
{
static void Main(string[] args)
{
BenchmarkRunner.Run<UUIDFormatterBenchmarks>();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Nullable>enable</Nullable>
<OutputType>Exe</OutputType>
<DebugType>pdbonly</DebugType>
<DebugSymbols>true</DebugSymbols>
<LangVersion>preview</LangVersion>
<AnalysisLevel>preview</AnalysisLevel>
<ImplicitUsings>enable</ImplicitUsings>
<PlatformTarget>AnyCPU</PlatformTarget>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>UUIDSerializationMessagePackBenchmarks</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\UUID\UUID.csproj" />
<ProjectReference Include="..\..\src\UUID.Serialization.MessagePack\UUID.Serialization.MessagePack.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="MessagePack" Version="3.1.1" />
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Order;
using MessagePack;
using MessagePack.Resolvers;

namespace UUIDSerializationMessagePackBenchmarks
{
[RankColumn]
[MemoryDiagnoser]
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
[SimpleJob(RuntimeMoniker.Net90, launchCount: 1, warmupCount: 2, iterationCount: 3)]
public class UUIDFormatterBenchmarks
{
private readonly MessagePackSerializerOptions _options;
private readonly UUID _uuid;
private readonly byte[] _serializedUuid;
private readonly List<UUID> _uuidList;
private readonly byte[] _serializedUuidList;
private readonly Dictionary<UUID, string> _uuidDictionary;
private readonly byte[] _serializedUuidDictionary;
private readonly TestModel _model;
private readonly byte[] _serializedModel;

public UUIDFormatterBenchmarks()
{
_options = MessagePackSerializerOptions.Standard
.WithResolver(CompositeResolver.Create(
UUIDResolver.Instance,
StandardResolver.Instance
));

_uuid = new UUID();
_serializedUuid = MessagePackSerializer.Serialize(_uuid, _options);

_uuidList = new List<UUID> { new(), new(), new() };
_serializedUuidList = MessagePackSerializer.Serialize(_uuidList, _options);

_uuidDictionary = new Dictionary<UUID, string>
{
{ new UUID(), "Value1" },
{ new UUID(), "Value2" },
{ new UUID(), "Value3" }
};
_serializedUuidDictionary = MessagePackSerializer.Serialize(_uuidDictionary, _options);

_model = new TestModel
{
Id = new UUID(),
Name = "Test",
Items = new List<UUID> { new(), new() }
};
_serializedModel = MessagePackSerializer.Serialize(_model, _options);
}

[Benchmark]
public byte[] Serialize_SingleUUID()
{
return MessagePackSerializer.Serialize(_uuid, _options);
}

[Benchmark]
public UUID Deserialize_SingleUUID()
{
return MessagePackSerializer.Deserialize<UUID>(_serializedUuid, _options);
}

[Benchmark]
public byte[] Serialize_UUIDList()
{
return MessagePackSerializer.Serialize(_uuidList, _options);
}

[Benchmark]
public List<UUID> Deserialize_UUIDList()
{
return MessagePackSerializer.Deserialize<List<UUID>>(_serializedUuidList, _options);
}

[Benchmark]
public byte[] Serialize_UUIDDictionary()
{
return MessagePackSerializer.Serialize(_uuidDictionary, _options);
}

[Benchmark]
public Dictionary<UUID, string> Deserialize_UUIDDictionary()
{
return MessagePackSerializer.Deserialize<Dictionary<UUID, string>>(_serializedUuidDictionary, _options);
}

[Benchmark]
public byte[] Serialize_ComplexModel()
{
return MessagePackSerializer.Serialize(_model, _options);
}

[Benchmark]
public TestModel Deserialize_ComplexModel()
{
return MessagePackSerializer.Deserialize<TestModel>(_serializedModel, _options);
}
}

[MessagePackObject]
public class TestModel
{
[Key(0)]
public UUID Id { get; set; }

[Key(1)]
public string Name { get; set; } = "";

[Key(2)]
public List<UUID> Items { get; set; } = new();
}
}
88 changes: 88 additions & 0 deletions src/UUID.Serialization.MessagePack/Formatters/UUIDFormatter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
using MessagePack;
using MessagePack.Formatters;
using System.Buffers;

namespace System
{
/// <summary>
/// MessagePack formatter for UUID type.
/// Handles serialization and deserialization of UUID values in MessagePack format.
/// </summary>
/// <remarks>
/// This formatter serializes UUID as a 16-byte binary array where:
/// - First 8 bytes contain the timestamp
/// - Last 8 bytes contain the random value
///
/// The formatter supports both modern (.NET 6+) and legacy (.NET Framework) serialization methods
/// through conditional compilation.
/// </remarks>
public class UUIDFormatter : IMessagePackFormatter<UUID>
{
/// <summary>
/// Deserializes a UUID from MessagePack format.
/// </summary>
/// <param name="reader">The MessagePack reader containing the serialized data.</param>
/// <param name="options">Serializer options for customizing the deserialization process.</param>
/// <returns>A deserialized UUID instance.</returns>
/// <exception cref="MessagePackSerializationException">
/// Thrown when:
/// - The input is null
/// - The input length is not exactly 16 bytes
/// - The MessagePack format is invalid
/// </exception>
/// <remarks>
/// The deserialization process expects a binary format with exactly 16 bytes.
/// The bytes are interpreted as two 8-byte segments for timestamp and random values.
/// </remarks>
public UUID Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options)
{
ReadOnlySequence<byte>? bin = reader.ReadBytes();

if (bin == null)
{
throw new MessagePackSerializationException($"Unexpected msgpack code {MessagePackCode.Nil} ({MessagePackCode.ToFormatName(MessagePackCode.Nil)}) encountered.");
}

byte[] bytes = bin.Value.ToArray();
if (bytes.Length != 16)
{
throw new MessagePackSerializationException($"UUID must be exactly 16 bytes long, but was {bytes.Length} bytes.");
}

ulong timestamp = BitConverter.ToUInt64(bytes, 0);
ulong random = BitConverter.ToUInt64(bytes, 8);

return new UUID(timestamp, random);
}

/// <summary>
/// Serializes a UUID to MessagePack format.
/// </summary>
/// <param name="writer">The MessagePack writer to write the serialized data to.</param>
/// <param name="value">The UUID value to serialize.</param>
/// <param name="options">Serializer options for customizing the serialization process.</param>
/// <remarks>
/// The serialization process writes the UUID as a 16-byte binary array.
/// Uses different approaches based on the target framework:
/// - For .NET 6+ and .NET Standard 2.1: Uses Span-based BitConverter methods
/// - For other frameworks: Uses traditional byte array methods with Buffer.BlockCopy
/// </remarks>
public void Serialize(ref MessagePackWriter writer, UUID value, MessagePackSerializerOptions options)
{
const int Length = 16;
byte[] bytes = new byte[Length];

#if NETCOREAPP || NETSTANDARD2_1
BitConverter.TryWriteBytes(bytes.AsSpan(0), value.Timestamp);
BitConverter.TryWriteBytes(bytes.AsSpan(8), value.Random);
#else
byte[] timestampBytes = BitConverter.GetBytes(value.Timestamp);
byte[] randomBytes = BitConverter.GetBytes(value.Random);
Buffer.BlockCopy(timestampBytes, 0, bytes, 0, 8);
Buffer.BlockCopy(randomBytes, 0, bytes, 8, 8);
#endif

writer.Write(bytes);
}
}
}
96 changes: 96 additions & 0 deletions src/UUID.Serialization.MessagePack/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# UUID.Serialization.MessagePack

A MessagePack serialization provider for the UUID library.

## Installation

```bash
dotnet add package UUID.Serialization.MessagePack
```

## Usage

### Basic Usage

```csharp
using MessagePack;
using MessagePack.Resolvers;

// Register the resolver globally
var options = MessagePackSerializerOptions.Standard
.WithResolver(CompositeResolver.Create(
UUIDResolver.Instance,
StandardResolver.Instance
));

// Serialize
var uuid = new UUID();
byte[] bytes = MessagePackSerializer.Serialize(uuid, options);

// Deserialize
UUID deserializedUuid = MessagePackSerializer.Deserialize<UUID>(bytes, options);
```

### With Models

```csharp
public class UserModel
{
[Key(0)]
public UUID Id { get; set; }

[Key(1)]
public string Name { get; set; }
}

// Register resolver
var options = MessagePackSerializerOptions.Standard
.WithResolver(CompositeResolver.Create(
UUIDResolver.Instance,
StandardResolver.Instance
));

// Serialize model
var user = new UserModel
{
Id = new UUID(),
Name = "John Doe"
};
byte[] bytes = MessagePackSerializer.Serialize(user, options);

// Deserialize model
UserModel deserializedUser = MessagePackSerializer.Deserialize<UserModel>(bytes, options);
```

## Features

- Support for null handling
- Thread-safe implementation
- Comprehensive error handling
- Efficient binary serialization
- Full documentation with XML comments
- Seamless integration with MessagePack
- Custom error messages for better debugging

## Error Handling

The formatter provides detailed error messages for common scenarios:

- Null values: "Cannot convert null value to UUID"
- Invalid byte length: "UUID must be exactly 16 bytes long"
- Incorrect MessagePack types: Shows expected vs actual type
- Invalid binary format: Includes details about the attempted deserialization

## Requirements

- UUID library
- MessagePack 2.5.129 or later
- Supports .NET 6.0+, .NET Standard 2.0+, and .NET Framework 4.8+

## Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

## License

This project is licensed under the terms of the LICENSE file included in the root directory.
Loading

0 comments on commit 95b9c13

Please sign in to comment.