PolyType is a practical generic programming library for .NET. It facilitates the rapid development of feature-complete, high-performance libraries that interact with user-defined types. This includes serializers, structured loggers, mappers, validators, parsers, random generators, and equality comparers. Its built-in source generator ensures that any library built on top of PolyType gets Native AOT support for free.
The project is a port of the TypeShape library for F#, adapted to patterns and idioms available in C#. The name PolyType is a reference to polytypic programming, another term for generic programming.
See the project website for additional background and API documentation.
You can try the library by installing the PolyType
NuGet package:
$ dotnet add package PolyType
which includes the core types and source generator for generating type shapes:
using PolyType;
[GenerateShape]
public partial record Person(string name, int age);
Doing this will augment Person
with an implementation of the IShapeable<Person>
interface. This suffices to make Person
usable with any library that targets the PolyType core abstractions. You can try this out by installing the built-in example libraries:
$ dotnet add package PolyType.Examples
Here's how the same value can be serialized to three separate formats.
using PolyType.Examples.JsonSerializer;
using PolyType.Examples.CborSerializer;
using PolyType.Examples.XmlSerializer;
Person person = new("Pete", 70);
JsonSerializerTS.Serialize(person); // {"Name":"Pete","Age":70}
XmlSerializer.Serialize(person); // <value><Name>Pete</Name><Age>70</Age></value>
CborSerializer.EncodeToHex(person); // A2644E616D656450657465634167651846
Since the application uses a source generator to produce the shape for Person
, it is fully compatible with Native AOT. See the shape providers article for more details on how to use the library with your types.
As a library author, PolyType makes it easy to write high-performance, feature-complete components by targeting its core abstractions. For example, a parser API using PolyType might look as follows:
public static class MyFancyParser
{
public static T? Parse<T>(string myFancyFormat) where T : IShapeable<T>;
}
The IShapeable<T>
constraint indicates that the parser only works with types augmented with PolyType metadata. This metadata can be provided using the PolyType source generator:
Person? person = MyFancyParser.Parse<Person>(format); // Compiles
[GenerateShape] // Generate an IShapeable<TPerson> implementation
partial record Person(string name, int age, List<Person> children);
For more information see:
- The core abstractions document for an overview of the core programming model.
- The shape providers document for an overview of the built-in shape providers and their APIs.
- The generated API documentation for the project.
- The
PolyType.Examples
project for advanced examples of libraries built on top of PolyType.
The repo includes a JSON serializer built on top of the Utf8JsonWriter
/Utf8JsonReader
primitives provided by System.Text.Json. At the time of writing, the full implementation is just under 1200 lines of code but exceeds STJ's built-in JsonSerializer
both in terms of supported types and performance.
Here's a benchmark comparing System.Text.Json
with the included PolyType implementation:
Method | Mean | Ratio | Allocated | Alloc Ratio |
---|---|---|---|---|
Serialize_StjReflection | 491.9 ns | 1.00 | 312 B | 1.00 |
Serialize_StjSourceGen | 467.0 ns | 0.95 | 312 B | 1.00 |
Serialize_StjSourceGen_FastPath | 227.2 ns | 0.46 | - | 0.00 |
Serialize_PolyTypeReflection | 277.9 ns | 0.57 | - | 0.00 |
Serialize_PolyTypeSourceGen | 273.6 ns | 0.56 | - | 0.00 |
Method | Mean | Ratio | Allocated | Alloc Ratio |
---|---|---|---|---|
Deserialize_StjReflection | 1,593.0 ns | 1.00 | 1024 B | 1.00 |
Deserialize_StjSourceGen | 1,530.3 ns | 0.96 | 1000 B | 0.98 |
Deserialize_PolyTypeReflection | 773.1 ns | 0.49 | 440 B | 0.43 |
Deserialize_PolyTypeSourceGen | 746.7 ns | 0.47 | 440 B | 0.43 |
Even though both serializers target the same underlying reader and writer types, the PolyType implementation is ~75% faster for serialization and ~100% faster for deserialization, when compared with System.Text.Json's metadata serializer. As expected, fast-path serialization is still fastest since its implementation is fully inlined.
The following code bases are based upon PolyType and may be worth checking out.
- Nerdbank.MessagePack - a MessagePack library with performance to rival MessagePack-CSharp, and greater simplicity and additional features.
The repo consists of the following projects:
- The core
PolyType
library containing:- The core abstractions defining the type model.
- The reflection provider implementation.
- The model classes used by the source generator.
- The
PolyType.SourceGenerator
project contains the built-in source generator implementation. - The
PolyType.Roslyn
library exposes a set of components for extracting data models from Roslyn type symbols. Used as the foundation for the built-in source generator. PolyType.Examples
containing library examples:- A serializer built on top of System.Text.Json,
- A serializer built on top of System.Xml,
- A serializer built on top of System.Formats.Cbor,
- A
ConfigurationBinder
like implementation, - A simple pretty-printer for .NET values,
- A generic random value generator based on
System.Random
, - A JSON schema generator for .NET types,
- An object cloning function,
- A structural
IEqualityComparer<T>
generator for POCOs and collections, - An object validator in the style of System.ComponentModel.DataAnnotations.
- A simple .NET object mapper.
- The
applications
folder contains sample Native AOT console applications.