PolyType

repository·main·Indexed 42 days ago

https://github.com/eiriktsarpalis/polytype

A generic programming library for .NET that provides abstractions and source generators for building high-performance, type-interfacing libraries such as serializers and mappers. It features built-in Native AOT support and includes PolyType.Roslyn for extracting data models from C# types using the Roslyn symbol API.

Tokens
16.9K
Snippets
36
Records
62
Agent score
55%

What's inside PolyType

  1. Overview of PolyType

    main

    PolyType is a generic programming library for .NET designed to facilitate the rapid development of high-performance libraries that interact with user-defined types. It is particularly useful for building serializers, RPC frameworks, structured loggers, mappers, validators, parsers, random generators, and equality comparers.

    Key features include:

    • Native AOT Support: Its built-in source generator ensures that libraries built on top of PolyType receive Native AOT support for free.
    • High Performance: Designed for performance-critical applications like EDI mapping engines or high-speed serializers.
    • Portability: Adapted from the TypeShape library (F#) for C# idioms.
  2. Overview of PolyType core abstractions

    main
    PolyType provides a set of core abstractions designed for building datatype-generic components. The primary interfaces used to inspect and traverse type structures are ITypeShape, IPropertyShape, and TypeShapeVisitor. These abstractions allow developers to programmatically interact with various type definitions (such as collections, enums, unions, and functions) in a structured way. Most of these core interfaces are located in the PolyType.Abstractions namespace.
  3. Overview of PolyType shape kinds

    main

    PolyType classifies .NET types into eight distinct type shape kinds. Each kind is represented by a specific interface in PolyType.Abstractions:

    • Object (IObjectTypeShape): General object types with properties and constructors.
    • Enumerable (IEnumerableTypeShape): Collection types implementing IEnumerable<T>.
    • Dictionary (IDictionaryTypeShape): Key-value collection types.
    • Enum (IEnumTypeShape): Enum types.
    • Optional (IOptionalTypeShape): Nullable value types (e.g., int?) and F# option types.
    • Surrogate (ISurrogateTypeShape): Types that define a marshaller to a surrogate type.
    • Union (IUnionTypeShape): Polymorphic type hierarchies or F# discriminated union types.
    • Function (IFunctionTypeShape): Delegate and F# function types.
  4. Define associated types for AOT compatibility

    main

    Associated types allow you to leap from an ITypeShape<T> to an associated shape (like a converter) without using reflection. This is critical for AOT/Source Generation environments.

    • Registration: Use [AssociatedTypeShape] on the originating type if both types are in the same assembly. If the associated type is in a different assembly, use [TypeShapeExtensionAttribute.AssociatedTypes] in the assembly declaring the associated type.
    • Visibility: Associated types should be public to ensure their shapes are available when the data type is used in other assemblies.
    • Partial Shapes: The SourceGenTypeShapeProvider can produce partial shapes (e.g., only the ability to activate an instance without defining all properties), which is useful for reducing output size in serializers.
  5. How the PolyType source generator works

    main

    PolyType uses a Roslyn incremental source generator to emit shape metadata at compile time. The generation process follows a three-stage pipeline:

    1. Parsing: The generator traverses the compilation to find types annotated with [GenerateShape] or [GenerateShapeFor<T>]. It extracts type data into an incremental-generator-safe model.
    2. Model mapping: The extracted data is mapped into a TypeShapeProviderModel which captures all necessary details (properties, constructors, collection semantics, enums) without holding onto Roslyn symbols.
    3. Source formatting: The generator emits C# source files, including a project-wide SourceGenTypeShapeProvider and individual files for each generated type shape.

    For details on how to use these shapes in your code, refer to the Shape providers guide.

  6. Configure Object Type properties and fields

    main

    Types that don't match other categories map to IObjectTypeShape. This includes primitives, string, DateTimeOffset, and Guid (which map trivially).

    Property Resolution Rules:

    • Public members: All public properties and fields are included unless ignored via PropertyShapeAttribute.
    • Non-public members: Included only if explicitly annotated with PropertyShapeAttribute.
    • DataContract: If DataContractAttribute is present, only members with DataMemberAttribute or PropertyShapeAttribute are included.
    • Inheritance: Base type members are included; derived class members take precedence (shadowing).
    • Unsupported: Pointers and ref structs are always skipped.
  7. Understand Dictionary Type construction strategies

    main

    Dictionary types (implementing IDictionary<K,V>, IReadOnlyDictionary<K,V>, or IDictionary) are assigned a CollectionConstructionStrategy based on the following priority:

    1. Mutable: Types with public parameterless constructors that expose Add and indexer methods, or implement mutable IDictionary interfaces.
    2. Parameterized: Immutable/frozen types, or types with public constructors accepting ReadOnlySpan<KeyValuePair<K,V>> or IEnumerable<KeyValuePair<K,V>>.
    3. Parameterized: Types annotated with CollectionBuilderAttribute.
    4. None: All other dictionary types.

    Note: PolyType automatically looks for overloads accepting IEqualityComparer<Key> or IComparer<Key> and maps them to CollectionConstructionOptions<TKey>.Capacity or similar settings. For parameterless constructors, it also looks for int capacity parameters.

  8. Understand Constructor Shape Resolution

    main

    PolyType resolves constructors for IObjectTypeShape using these rules:

    1. Selection: Only public constructors are considered by default, but ConstructorShapeAttribute can be used to opt-in non-public constructors.
    2. Preference: If multiple exist, PolyType prefers the one that:
      • Minimizes required parameters not matching property shapes.
      • Maximizes parameters matching read-only properties/fields.
      • Minimizes total parameter count.
    3. Parameter Matching: A parameter matches a property if they have the same type and names match (ignoring PascalCase vs camelCase).
    4. Resulting Shape:
      • If a parameterless constructor is selected and there are no required/init-only properties, it is an IConstructorShape that relies on property setters.
      • Otherwise, it is a parameterized IConstructorShape. Parameterized constructors include all settable members not covered by constructor parameters.
  9. How the AOT size check works

    main

    The AOT size check tool measures the file size of the Native AOT publish output from tests/SizeTrackingApp.AOT and compares it against per-platform baselines stored in tests/SizeTrackingApp.AOT/aot-size-baselines.json.

    The check is bidirectional: it fails if there is significant growth or significant shrinkage. This allows the baseline window to be ratcheted tighter as the project becomes leaner.

  10. Construct objects using Constructor Shapes

    main

    For types that are immutable or only expose parameterized constructors, PolyType uses the IConstructorShape abstraction. This allows you to construct new instances from scratch.

    Construction is handled via an opaque, mutable token called TArgumentState. This state encapsulates all parameters required by the constructor. The process follows these steps:

    1. Initialize State: Call GetArgumentStateConstructor() to get a default instance of the TArgumentState.
    2. Populate State: Use IParameterShape<TArgumentState, TParameterType>.GetSetter() to populate the state token with values.
    3. Execute Constructor: Call GetParameterizedConstructor() passing the populated state to create the final object.

    Key Components:

    • IObjectTypeShape<T>.Constructor: Access the constructor shape for a type.
    • IConstructorShape<TDeclaringType, TArgumentState>: Defines the state lifecycle and the final constructor call.
    • IParameterShape<TArgumentState, TParameterType>: Provides setters to populate the TArgumentState token.
    • TArgumentState: An opaque token (often a value tuple) representing the collection of constructor arguments.
    // Example: Implementing a constructor shape for a record
    record MyPoco(string y, int x = 42);
    
    class MyPocoConstructorShape : IConstructorShape<MyPoco, (string, int)>
    {
        // Returns a default state: (null, 42)
        public Func<(string, int)> GetArgumentStateConstructor() => () => (null!, 42);
        
        // Creates the record from the state
        public Func<(string, int), MyPoco> GetParameterizedConstructor() => state => new MyPoco(state.Item1, state.Item2);
    }
  11. Understand Enumerable Type construction strategies

    main

    Non-dictionary types implementing IEnumerable<T>, IEnumerable (non-generic), IAsyncEnumerable<T>, arrays, Memory<T>, ReadOnlyMemory<T>, inline arrays, or structs with fixed-size buffers are mapped to IEnumerableTypeShape.

    Construction strategies follow this priority:

    1. Mutable: Public parameterless constructors with Add methods, or implementing mutable ICollection interfaces.
    2. Parameterized: Immutable/frozen collections, or types with public constructors accepting ReadOnlySpan<TElement> or IEnumerable<TElement>.
    3. Parameterized: Types annotated with CollectionBuilderAttribute.
    4. None: All other enumerable types.