Type shapes
This library leverages PolyType as a source generator that provides fast startup time and a consistent set of attributes that may be used for many purposes within your application.
Recommended configuration
PolyType is trim-safe and NativeAOT ready, particularly when used in its recommended configuration, where you apply GenerateShapeAttribute on the root type of your data model.
[GenerateShape]
public partial record Tree(string Name, Fruit[] Fruits);
public record Fruit(double Weight, string Color);
Witness classes
If you need to directly serialize a type that isn't declared in your project and is not annotated with GenerateShapeAttribute, you can define another class in your own project to provide that shape. Doing so leads to default serialization rules being applied to the type (e.g. only public members are serialized).
For this example, suppose you consume a FamilyTree
type from a library that you don't control and did not annotate their type for serialization.
In your own project, you can define this witness type and use it to serialize an external type.
// This class declared in another assembly, unattributed and outside of your control.
public class FamilyTree
{
}
// Within your own assembly, define a 'witness' class with one or more shapes generated for external types.
[GenerateShapeFor<FamilyTree>]
partial class Witness;
void Serialize()
{
var familyTree = new FamilyTree();
var serializer = new MessagePackSerializer();
// Serialize the FamilyTree instance using the shape generated from your witness class.
byte[] msgpack = serializer.Serialize<FamilyTree, Witness>(familyTree);
}
Note the only special bit is providing the Witness
class as a type argument to the Serialize
method.
The name of the witness class is completely inconsequential.
A witness class may have any number of GenerateShapeForAttribute<T> attributes on it.
It is typical (but not required) for an assembly to have at most one witness class, with all the external types listed on it that you need to serialize as top-level objects.
You do not need a witness class for an external type to reference that type from a graph that is already rooted in a type that is attributed.
Fallback configuration
In the unlikely event that you have a need to serialize a type that does not have a shape source-generated for it, you can use the conventional reflection approach of serialization with Nerdbank.MessagePack, if you do not need to run in a trimmed app.
void SerializeUnshapedType()
{
Person person = new("Andrew", "Arnott");
MessagePackSerializer serializer = new();
byte[] msgpack = serializer.Serialize(person, ReflectionTypeShapeProvider.Default);
Person? deserialized = serializer.Deserialize<Person>(msgpack, ReflectionTypeShapeProvider.Default);
}
record Person(string FirstName, string LastName);
Source generated data models
If your data models are themselves declared by a source generator, the PolyType source generator will be unable to emit type shapes for your data models in the same compilation. Using the ReflectionTypeShapeProvider.Default is one way you can workaround this, at the cost of somewhat slower serialization (especially the first time due to reflection), but this does not always work in a trimmed application. And it adds some limitations to what types can be serialized in a NativeAOT application where dynamic code cannot run.
Instead of falling back to reflection, you can still use the PolyType source generator by declaring your data types in another assembly that your serialization code then references. For example, if assembly "A" declares your data types via a source generator (e.g. Vogen), assembly "B" can reference "A", and then use a Witness type (described above) to source generate the type shapes for all your data types.
Still another option to get your source generated data type to be serializable may be to define a marshaler to a surrogate type.
Working with Vogen
Vogen is a source generator that wraps primitive types in custom structs that can add validation and another level of type safety to your data models.
Important
Use Vogen 8.0.3 or later, which emits PolyType marshalers so that data types are serialized without extranneous wrappers.
With Vogen, you have the two options described in the above section, to either declare your data models in a separate project or use the reflection type shape provider. Here is what those two worlds look like:
Data models in a separate project
Consider the following data models in an auxiliary project:
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using Vogen;
namespace VogenDataTypes;
[ValueObject<int>]
public partial struct CustomerId;
public record Customer
{
public required CustomerId Id { get; set; }
public required string Name { get; set; }
}
Your serialization code in a referencing assembly then looks like this:
partial class VogenConsumer
{
static MessagePackSerializer serializer = new();
#pragma warning disable NBMsgPack051 // We cannot use the type constraint overload because Vogen requires that we use Witness types.
void Serialize(Customer customer)
{
byte[] msgpack = serializer.Serialize(customer, Witness.GeneratedTypeShapeProvider);
}
// We have to generate the type shapes *here* so that PolyType's source generator
// can see the Vogen-generated code from the other project.
[GenerateShapeFor<Customer>]
partial class Witness;
}
Data models in the same project
When your Vogen data models are in the same project, you must use the ReflectionTypeShapeProvider.Default
First, define the data model like this:
[ValueObject<int>]
public partial struct CustomerId;
public record Customer
{
public required CustomerId Id { get; set; }
public required string Name { get; set; }
}
These data models are almost the same as the auxiliary assembly sample, except that Customer
does not have to be partial
nor carry the GenerateShapeAttribute.
Your serialization code in the same assembly then uses ReflectionTypeShapeProvider.Default and looks like this:
class VogenConsumer
{
static MessagePackSerializer serializer = new();
void Serialize(Customer customer)
{
// Use the reflection-based type shape provider to handle Vogen-generated types
// because the PolyType source generator cannot see Vogen's source generated code.
// Alternatively, define the data types in another project and reference that project
// from here, then PolyType source generated type shapes will be available.
byte[] msgpack = serializer.Serialize(customer, ReflectionTypeShapeProvider.Default);
}
}