Skip to content

Commit

Permalink
Add support for stateful converters via SerializationContext
Browse files Browse the repository at this point in the history
  • Loading branch information
AArnott committed Dec 22, 2024
1 parent c462dd8 commit 432e731
Show file tree
Hide file tree
Showing 13 changed files with 772 additions and 358 deletions.
36 changes: 35 additions & 1 deletion docfx/docs/custom-converters.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ The @PolyType.GenerateShapeAttribute`1 is what enables `FooConverter` to be a "p
Arrays of a type require a shape of their own.
So even if you define your type `MyType` with @PolyType.GenerateShapeAttribute`1, serializing `MyType[]` would require a witness type and attribute. For example:


# [.NET](#tab/net)

[!code-csharp[](../../samples/CustomConverters.cs#ArrayWitnessOnFormatterNET)]
Expand Down Expand Up @@ -119,6 +118,41 @@ The following sample demonstrates using the @Nerdbank.MessagePack.MessagePackStr

[!code-csharp[](../../samples/CustomConverters.cs#MessagePackStringUser)]

### Stateful converters

Converters are usually stateless, meaning that they have no fields and serialize/deserialize strictly on the inputs provided them via their parameters.

When converters have stateful fields, they cannot be used concurrently with different values in those fields.
Creating multiple instances of those converters with different values in those fields requires creating unique instances of @Nerdbank.MessagePack.MessagePackSerializer which each incur a startup cost while they create and cache the rest of the converters necessary for your data model.

For higher performance, configure one @Nerdbank.MessagePack.MessagePackSerializer instance with one set of converters.
Your converters can be stateful by accessing state in the @Nerdbank.MessagePack.SerializationContext parameter instead of fields on the converter itself.

For example, suppose your custom converter serializes data bound for a particular RPC connection and must access state associated with that connection.
This can be achieved as follows:

1. Store the state in the @Nerdbank.MessagePack.SerializationContext via its @Nerdbank.MessagePack.SerializationContext.Item(System.Object)?displayProperty=nameWithType indexer.
1. Apply that @Nerdbank.MessagePack.SerializationContext to a @Nerdbank.MessagePack.MessagePackSerializer by setting its @Nerdbank.MessagePack.MessagePackSerializer.StartingContext property.
1. Your custom converter can then retrieve that state during serialization/deserialization via that same @Nerdbank.MessagePack.SerializationContext.Item(System.Object)?displayProperty=nameWithType indexer.

# [.NET](#tab/net)

[!code-csharp[](../../samples/CustomConverters.cs#StatefulNET)]

# [.NET Standard](#tab/netfx)

[!code-csharp[](../../samples/CustomConverters.cs#StatefulNETFX)]

---

When the state object stored in the @Nerdbank.MessagePack.SerializationContext is a mutable reference type, the converters *may* mutate it such that they or others can observe those changes later.
Consider the thread-safety implications of doing this if that same mutable state object is shared across multiple serializations that may happen on different threads in parallel.

Converters that change the state dictionary itself (by using @"Nerdbank.MessagePack.SerializationContext.Item(System.Object)?displayProperty=nameWithType") can expect those changes to propagate only to their callees.

Strings can serve as convenient keys, but may collide with the same string used by another part of the data model for another purpose.
Make your strings sufficiently unique to avoid collisions, or use a `static readonly object MyKey = new object()` field that you expose such that all interested parties can access the object for a key that is guaranteed to be unique.

### Async converters

@Nerdbank.MessagePack.MessagePackConverter`1 is an abstract class that requires a derived converter to implement synchronous @Nerdbank.MessagePack.MessagePackConverter`1.Write* and @Nerdbank.MessagePack.MessagePackConverter`1.Read* methods.
Expand Down
91 changes: 91 additions & 0 deletions samples/CustomConverters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -454,3 +454,94 @@ public override void Write(ref MessagePackWriter writer, in MyCustomType? value,
}
#endregion
}

namespace Stateful
{
#if NET
#region StatefulNET
class Program
{
static void Main()
{
MessagePackSerializer serializer = new()
{
StartingContext = new SerializationContext
{
["ValueMultiplier"] = 3,
},
};
SpecialType original = new(5);
Console.WriteLine($"Original value: {original}");
byte[] msgpack = serializer.Serialize(original);
Console.WriteLine(MessagePackSerializer.ConvertToJson(msgpack));
SpecialType deserialized = serializer.Deserialize<SpecialType>(msgpack);
Console.WriteLine($"Deserialized value: {deserialized}");
}
}

class StatefulConverter : MessagePackConverter<SpecialType>
{
public override SpecialType Read(ref MessagePackReader reader, SerializationContext context)
{
int multiplier = (int)context["ValueMultiplier"]!;
int serializedValue = reader.ReadInt32();
return new SpecialType(serializedValue / multiplier);
}

public override void Write(ref MessagePackWriter writer, in SpecialType value, SerializationContext context)
{
int multiplier = (int)context["ValueMultiplier"]!;
writer.Write(value.Value * multiplier);
}
}

[GenerateShape]
[MessagePackConverter(typeof(StatefulConverter))]
partial record struct SpecialType(int Value);
#endregion
#else
#region StatefulNETFX
class Program
{
static void Main()
{
MessagePackSerializer serializer = new()
{
StartingContext = new SerializationContext
{
["ValueMultiplier"] = 3,
},
};
SpecialType original = new(5);
Console.WriteLine($"Original value: {original}");
byte[] msgpack = serializer.Serialize(original, Witness.ShapeProvider);
Console.WriteLine(MessagePackSerializer.ConvertToJson(msgpack));
SpecialType deserialized = serializer.Deserialize<SpecialType>(msgpack, Witness.ShapeProvider);
Console.WriteLine($"Deserialized value: {deserialized}");
}
}

class StatefulConverter : MessagePackConverter<SpecialType>
{
public override SpecialType Read(ref MessagePackReader reader, SerializationContext context)
{
int multiplier = (int)context["ValueMultiplier"]!;
int serializedValue = reader.ReadInt32();
return new SpecialType(serializedValue / multiplier);
}

public override void Write(ref MessagePackWriter writer, in SpecialType value, SerializationContext context)
{
int multiplier = (int)context["ValueMultiplier"]!;
writer.Write(value.Value * multiplier);
}
}

[MessagePackConverter(typeof(StatefulConverter))]
partial record struct SpecialType(int Value);

[GenerateShape<SpecialType>]
partial class Witness;
#endregion
#endif
}
Loading

0 comments on commit 432e731

Please sign in to comment.