polytype by Eirik Tsarpalis
NuGet / site data
Details
Info
Name: polytype
Practical Generic Programming for C#
Author: Eirik Tsarpalis
NuGet: https://www.nuget.org/packages/polytype/
You can find more details at https://github.com/eiriktsarpalis/PolyType
Original Readme
PolyType

PolyType is a practical datatype-generic programming library for .NET types. It is a direct adaptation of the TypeShape library for F#, adapted to patterns and idioms available in C#. See the project website for additional background and API documentation.
Quick Start
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.
Introduction
PolyType is a meta-library that facilitates rapid development of high performance datatype-generic programs. It exposes a simplified model for .NET types that makes it easy for library authors to publish production-ready components in just a few lines of code. The built-in source generator ensures that any library built on top of the PolyType abstractions gets Native AOT support for free.
As a library author, PolyType lets you write high performance, feature complete generic components that target 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>;
}
As an end user, PolyType lets you generate shape models for your own types that can be used with one or more supported libraries:
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.Examplesproject for advanced examples of libraries built on top of PolyType.
Case Study: Writing a JSON serializer
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.
Performance
Here's a benchmark comparing System.Text.Json with the included PolyType implementation:
Serialization
| 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 |
Deserialization
| 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.
Known libraries based on PolyType
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.
Project structure
The repo consists of the following projects:
- The core
PolyTypelibrary containing:- The core abstractions defining the type model.
- The reflection provider implementation.
- The model classes used by the source generator.
- The
PolyType.SourceGeneratorproject contains the built-in source generator implementation. - The
PolyType.Roslynlibrary exposes a set of components for extracting data models from Roslyn type symbols. Used as the foundation for the built-in source generator. PolyType.Examplescontaining 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
ConfigurationBinderlike 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
applicationsfolder contains sample Native AOT console applications.
About
Generating shape like reflection from classes. See PolyType.Examples for more details
How to use
Example (source csproj, source files)
- CSharp Project
- Program.cs
- Person.cs
This is the CSharp Project that references polytype
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="PolyType" Version="0.16.1" />
<PackageReference Include="PolyType.Examples" Version="0.16.1" />
</ItemGroup>
<PropertyGroup>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<CompilerGeneratedFilesOutputPath>$(BaseIntermediateOutputPath)\GX</CompilerGeneratedFilesOutputPath>
</PropertyGroup>
</Project>
This is the use of polytype in Program.cs
using PolyType.Examples.JsonSerializer;
using PolyType.Examples.CborSerializer;
using PolyType.Examples.XmlSerializer;
using ConsoleApp1;
using PolyType.Examples.Cloner;
Person person = new("Pete", 70);
Console.WriteLine(JsonSerializerTS.Serialize(person)); // {"Name":"Pete","Age":70}
Console.WriteLine(XmlSerializer.Serialize(person)); // <value><Name>Pete</Name><Age>70</Age></value>
Console.WriteLine(CborSerializer.EncodeToHex(person)); // A2644E616D656450657465634167651846
person.Childs = [new Person("Andrei", 55)];
person.Childs[0].ID = 1;
var q = Cloner.Clone(person);
person.Childs[0].ID = 10;
Console.WriteLine(q);
Console.WriteLine(person);
Console.WriteLine(q.Childs[0]);
Console.WriteLine(person.Childs[0]);
This is the use of polytype in Person.cs
using PolyType;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace ConsoleApp1;
[GenerateShape]
public partial record Person(string name, int age)
{
public Person[] Childs { get; set; } = [];
public int ID;
}
Generated Files
Those are taken from $(BaseIntermediateOutputPath)\GX
- ConsoleApp1.Person.ITypeShapeProviderOfT.g.cs
- GenerateShapeProvider.g.cs
- GenerateShapeProvider.Int32.g.cs
- GenerateShapeProvider.ITypeShapeProvider.g.cs
- GenerateShapeProvider.Person.g.cs
- GenerateShapeProvider.Person_Array.g.cs
- GenerateShapeProvider.String.g.cs
// <auto-generated/>
#nullable enable annotations
#nullable disable warnings
namespace ConsoleApp1
{
public partial record Person : global::PolyType.IShapeable<global::ConsoleApp1.Person>
{
static global::PolyType.Abstractions.ITypeShape<global::ConsoleApp1.Person> global::PolyType.IShapeable<global::ConsoleApp1.Person>.GetShape()
=> global::PolyType.SourceGenerator.GenerateShapeProvider.Default.Person;
}
}
// <auto-generated/>
#nullable enable annotations
#nullable disable warnings
namespace PolyType.SourceGenerator
{
internal partial class GenerateShapeProvider
{
private const global::System.Reflection.BindingFlags InstanceBindingFlags =
global::System.Reflection.BindingFlags.Public |
global::System.Reflection.BindingFlags.NonPublic |
global::System.Reflection.BindingFlags.Instance;
/// <summary>Gets the default instance of the <see cref="GenerateShapeProvider"/> class.</summary>
public static GenerateShapeProvider Default { get; } = new();
/// <summary>Initializes a new instance of the <see cref="GenerateShapeProvider"/> class.</summary>
public GenerateShapeProvider() { }
}
}
// <auto-generated/>
#nullable enable annotations
#nullable disable warnings
namespace PolyType.SourceGenerator
{
internal partial class GenerateShapeProvider
{
/// <summary>Gets the generated shape for specified type.</summary>
#nullable disable annotations // Use nullable-oblivious property type
public global::PolyType.Abstractions.ITypeShape<int> Int32 => _Int32 ??= Create_Int32();
#nullable enable annotations // Use nullable-oblivious property type
private global::PolyType.Abstractions.ITypeShape<int>? _Int32;
private global::PolyType.Abstractions.ITypeShape<int> Create_Int32()
{
return new global::PolyType.SourceGenModel.SourceGenObjectTypeShape<int>
{
Provider = this,
IsRecordType = false,
IsTupleType = false,
CreatePropertiesFunc = null,
CreateConstructorFunc = null,
};
}
}
}
// <auto-generated/>
#nullable enable annotations
#nullable disable warnings
namespace PolyType.SourceGenerator
{
internal partial class GenerateShapeProvider : global::PolyType.ITypeShapeProvider
{
/// <summary>
/// Gets the generated <see cref="global::PolyType.Abstractions.ITypeShape{T}" /> for the specified type.
/// </summary>
/// <typeparam name="T">The type for which a shape is requested.</typeparam>
/// <returns>
/// The generated <see cref="global::PolyType.Abstractions.ITypeShape{T}" /> for the specified type.
/// </returns>
public global::PolyType.Abstractions.ITypeShape<T>? GetShape<T>()
=> (global::PolyType.Abstractions.ITypeShape<T>?)GetShape(typeof(T));
/// <summary>
/// Gets the generated <see cref="global::PolyType.Abstractions.ITypeShape" /> for the specified type.
/// </summary>
/// <param name="type">The type for which a shape is requested.</param>
/// <returns>
/// The generated <see cref="global::PolyType.Abstractions.ITypeShape" /> for the specified type.
/// </returns>
public global::PolyType.Abstractions.ITypeShape? GetShape(global::System.Type type)
{
if (type == typeof(global::ConsoleApp1.Person[]))
return Person_Array;
if (type == typeof(int))
return Int32;
if (type == typeof(string))
return String;
if (type == typeof(global::ConsoleApp1.Person))
return Person;
return null;
}
}
}
// <auto-generated/>
#nullable enable annotations
#nullable disable warnings
namespace PolyType.SourceGenerator
{
internal partial class GenerateShapeProvider
{
/// <summary>Gets the generated shape for specified type.</summary>
#nullable disable annotations // Use nullable-oblivious property type
public global::PolyType.Abstractions.ITypeShape<global::ConsoleApp1.Person> Person => _Person ??= Create_Person();
#nullable enable annotations // Use nullable-oblivious property type
private global::PolyType.Abstractions.ITypeShape<global::ConsoleApp1.Person>? _Person;
private global::PolyType.Abstractions.ITypeShape<global::ConsoleApp1.Person> Create_Person()
{
return new global::PolyType.SourceGenModel.SourceGenObjectTypeShape<global::ConsoleApp1.Person>
{
Provider = this,
IsRecordType = true,
IsTupleType = false,
CreatePropertiesFunc = CreateProperties_Person,
CreateConstructorFunc = CreateConstructor_Person,
};
}
private global::PolyType.Abstractions.IPropertyShape[] CreateProperties_Person() => new global::PolyType.Abstractions.IPropertyShape[]
{
new global::PolyType.SourceGenModel.SourceGenPropertyShape<global::ConsoleApp1.Person, string>
{
Name = "name",
DeclaringType = (global::PolyType.Abstractions.IObjectTypeShape<global::ConsoleApp1.Person>)Person,
PropertyType = String,
Getter = static (ref global::ConsoleApp1.Person obj) => obj.name,
Setter = null,
AttributeProviderFunc = static () => typeof(global::ConsoleApp1.Person).GetProperty("name", InstanceBindingFlags, null, typeof(string), [], null),
IsField = false,
IsGetterPublic = true,
IsSetterPublic = false,
IsGetterNonNullable = true,
IsSetterNonNullable = false,
},
new global::PolyType.SourceGenModel.SourceGenPropertyShape<global::ConsoleApp1.Person, int>
{
Name = "age",
DeclaringType = (global::PolyType.Abstractions.IObjectTypeShape<global::ConsoleApp1.Person>)Person,
PropertyType = Int32,
Getter = static (ref global::ConsoleApp1.Person obj) => obj.age,
Setter = null,
AttributeProviderFunc = static () => typeof(global::ConsoleApp1.Person).GetProperty("age", InstanceBindingFlags, null, typeof(int), [], null),
IsField = false,
IsGetterPublic = true,
IsSetterPublic = false,
IsGetterNonNullable = true,
IsSetterNonNullable = false,
},
new global::PolyType.SourceGenModel.SourceGenPropertyShape<global::ConsoleApp1.Person, global::ConsoleApp1.Person[]>
{
Name = "Childs",
DeclaringType = (global::PolyType.Abstractions.IObjectTypeShape<global::ConsoleApp1.Person>)Person,
PropertyType = Person_Array,
Getter = static (ref global::ConsoleApp1.Person obj) => obj.Childs,
Setter = static (ref global::ConsoleApp1.Person obj, global::ConsoleApp1.Person[] value) => obj.Childs = value,
AttributeProviderFunc = static () => typeof(global::ConsoleApp1.Person).GetProperty("Childs", InstanceBindingFlags, null, typeof(global::ConsoleApp1.Person[]), [], null),
IsField = false,
IsGetterPublic = true,
IsSetterPublic = true,
IsGetterNonNullable = true,
IsSetterNonNullable = true,
},
new global::PolyType.SourceGenModel.SourceGenPropertyShape<global::ConsoleApp1.Person, int>
{
Name = "ID",
DeclaringType = (global::PolyType.Abstractions.IObjectTypeShape<global::ConsoleApp1.Person>)Person,
PropertyType = Int32,
Getter = static (ref global::ConsoleApp1.Person obj) => obj.ID,
Setter = static (ref global::ConsoleApp1.Person obj, int value) => obj.ID = value,
AttributeProviderFunc = static () => typeof(global::ConsoleApp1.Person).GetField("ID", InstanceBindingFlags),
IsField = true,
IsGetterPublic = true,
IsSetterPublic = true,
IsGetterNonNullable = true,
IsSetterNonNullable = true,
},
};
private global::PolyType.Abstractions.IConstructorShape CreateConstructor_Person()
{
return new global::PolyType.SourceGenModel.SourceGenConstructorShape<global::ConsoleApp1.Person, (string, int, global::ConsoleApp1.Person[], int, byte Flags)>
{
DeclaringType = (global::PolyType.Abstractions.IObjectTypeShape<global::ConsoleApp1.Person>)Person,
ParameterCount = 4,
GetParametersFunc = CreateConstructorParameters_Person,
DefaultConstructorFunc = null,
ArgumentStateConstructorFunc = static () => default((string, int, global::ConsoleApp1.Person[], int, byte Flags)),
ParameterizedConstructorFunc = static (ref (string, int, global::ConsoleApp1.Person[], int, byte Flags) state) => { var obj = new global::ConsoleApp1.Person(state.Item1, state.Item2); if ((state.Flags & 1) != 0) obj.Childs = state.Item3; if ((state.Flags & 2) != 0) obj.ID = state.Item4; return obj; },
AttributeProviderFunc = static () => typeof(global::ConsoleApp1.Person).GetConstructor(InstanceBindingFlags, new[] { typeof(string), typeof(int) }),
IsPublic = true,
};
}
private global::PolyType.Abstractions.IConstructorParameterShape[] CreateConstructorParameters_Person() => new global::PolyType.Abstractions.IConstructorParameterShape[]
{
new global::PolyType.SourceGenModel.SourceGenConstructorParameterShape<(string, int, global::ConsoleApp1.Person[], int, byte Flags), string>
{
Position = 0,
Name = "name",
ParameterType = String,
Kind = global::PolyType.Abstractions.ConstructorParameterKind.ConstructorParameter,
IsRequired = true,
IsNonNullable = true,
IsPublic = true,
HasDefaultValue = false,
DefaultValue = default!,
Setter = static (ref (string, int, global::ConsoleApp1.Person[], int, byte Flags) state, string value) => state.Item1 = value,
AttributeProviderFunc = static () => typeof(global::ConsoleApp1.Person).GetConstructor(InstanceBindingFlags, new[] { typeof(string), typeof(int) })?.GetParameters()[0],
},
new global::PolyType.SourceGenModel.SourceGenConstructorParameterShape<(string, int, global::ConsoleApp1.Person[], int, byte Flags), int>
{
Position = 1,
Name = "age",
ParameterType = Int32,
Kind = global::PolyType.Abstractions.ConstructorParameterKind.ConstructorParameter,
IsRequired = true,
IsNonNullable = true,
IsPublic = true,
HasDefaultValue = false,
DefaultValue = default,
Setter = static (ref (string, int, global::ConsoleApp1.Person[], int, byte Flags) state, int value) => state.Item2 = value,
AttributeProviderFunc = static () => typeof(global::ConsoleApp1.Person).GetConstructor(InstanceBindingFlags, new[] { typeof(string), typeof(int) })?.GetParameters()[1],
},
new global::PolyType.SourceGenModel.SourceGenConstructorParameterShape<(string, int, global::ConsoleApp1.Person[], int, byte Flags), global::ConsoleApp1.Person[]>
{
Position = 2,
Name = "Childs",
ParameterType = Person_Array,
Kind = global::PolyType.Abstractions.ConstructorParameterKind.PropertyInitializer,
IsRequired = false,
IsNonNullable = true,
IsPublic = true,
HasDefaultValue = false,
DefaultValue = default!,
Setter = static (ref (string, int, global::ConsoleApp1.Person[], int, byte Flags) state, global::ConsoleApp1.Person[] value) => { state.Item3 = value; state.Flags |= 1; },
AttributeProviderFunc = static () => typeof(global::ConsoleApp1.Person).GetProperty("Childs", InstanceBindingFlags, null, typeof(global::ConsoleApp1.Person[]), [], null),
},
new global::PolyType.SourceGenModel.SourceGenConstructorParameterShape<(string, int, global::ConsoleApp1.Person[], int, byte Flags), int>
{
Position = 3,
Name = "ID",
ParameterType = Int32,
Kind = global::PolyType.Abstractions.ConstructorParameterKind.FieldInitializer,
IsRequired = false,
IsNonNullable = true,
IsPublic = true,
HasDefaultValue = false,
DefaultValue = default,
Setter = static (ref (string, int, global::ConsoleApp1.Person[], int, byte Flags) state, int value) => { state.Item4 = value; state.Flags |= 2; },
AttributeProviderFunc = static () => typeof(global::ConsoleApp1.Person).GetField("ID", InstanceBindingFlags),
},
};
[global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_name")]
private static extern void Person_name_SetAccessor(global::ConsoleApp1.Person obj, string value);
[global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Method, Name = "set_age")]
private static extern void Person_age_SetAccessor(global::ConsoleApp1.Person obj, int value);
[global::System.Runtime.CompilerServices.UnsafeAccessor(global::System.Runtime.CompilerServices.UnsafeAccessorKind.Field, Name = "ID")]
private static extern ref int Person_ID_Accessor(global::ConsoleApp1.Person obj);
}
}
// <auto-generated/>
#nullable enable annotations
#nullable disable warnings
namespace PolyType.SourceGenerator
{
internal partial class GenerateShapeProvider
{
/// <summary>Gets the generated shape for specified type.</summary>
#nullable disable annotations // Use nullable-oblivious property type
public global::PolyType.Abstractions.ITypeShape<global::ConsoleApp1.Person[]> Person_Array => _Person_Array ??= Create_Person_Array();
#nullable enable annotations // Use nullable-oblivious property type
private global::PolyType.Abstractions.ITypeShape<global::ConsoleApp1.Person[]>? _Person_Array;
private global::PolyType.Abstractions.ITypeShape<global::ConsoleApp1.Person[]> Create_Person_Array()
{
return new global::PolyType.SourceGenModel.SourceGenEnumerableTypeShape<global::ConsoleApp1.Person[], global::ConsoleApp1.Person>
{
ElementType = Person,
ConstructionStrategy = global::PolyType.Abstractions.CollectionConstructionStrategy.Span,
DefaultConstructorFunc = null,
EnumerableConstructorFunc = null,
SpanConstructorFunc = static values => values.ToArray(),
GetEnumerableFunc = static obj => obj,
AddElementFunc = null,
Rank = 1,
Provider = this,
};
}
}
}
// <auto-generated/>
#nullable enable annotations
#nullable disable warnings
namespace PolyType.SourceGenerator
{
internal partial class GenerateShapeProvider
{
/// <summary>Gets the generated shape for specified type.</summary>
#nullable disable annotations // Use nullable-oblivious property type
public global::PolyType.Abstractions.ITypeShape<string> String => _String ??= Create_String();
#nullable enable annotations // Use nullable-oblivious property type
private global::PolyType.Abstractions.ITypeShape<string>? _String;
private global::PolyType.Abstractions.ITypeShape<string> Create_String()
{
return new global::PolyType.SourceGenModel.SourceGenObjectTypeShape<string>
{
Provider = this,
IsRecordType = false,
IsTupleType = false,
CreatePropertiesFunc = null,
CreateConstructorFunc = null,
};
}
}
}
Useful
Download Example (.NET C#)
Share polytype
https://ignatandrei.github.io/RSCG_Examples/v2/docs/polytype
aaa
Category "FunctionalProgramming" has the following generators:
2 dunet
3 Dusharp
6 N.SourceGenerators.UnionTypes
7 OneOf
9 polytype
10 rscg_demeter
13 Sera.Union
15 UnionGen