Specification

This document details how .NET types are mapped to type shapes. Both built-in shape providers (source generator and reflection provider) implement equivalent derivation logics that map arbitrary .NET types into type shapes of different kinds.

Overview

PolyType classifies .NET types into eight distinct type shape kinds, each represented by a specific interface:

Derivation Algorithm

PolyType maps types into individual shape kinds using the following rules:

Enum Types

A type is mapped to IEnumTypeShape if and only if it is an enum type.

Optional Types

A type is mapped to IOptionalTypeShape when:

  1. It is Nullable<T> (e.g., int?, DateTime?) or
  2. It is an F# option type

Function Types

A type is mapped to IFunctionTypeShape when:

  1. It is a delegate type or
  2. It is an F# function type (i.e. any type deriving from FSharpFunc<T,R>).

Surrogate Types

A type is mapped to ISurrogateTypeShape if and only if the type has been given an IMarshaler<TSource, TTarget> implementation to a surrogate type. This is typically configured via the Marshaller property on the TypeShapeAttribute, GenerateShapeAttribute, or TypeShapeExtensionAttribute attributes and doing so overrides the built-in shape kind inferred for the type.

Union Types

A type is mapped to IUnionTypeShape when:

  1. It is a class with DerivedTypeShapeAttribute annotations or
  2. It has DataContractAttribute with KnownTypeAttribute annotations or
  3. It is an F# union type.

Dictionary Types

A type is mapped to IDictionaryTypeShape when it implements:

  1. IDictionary<TKey, TValue> or
  2. IReadOnlyDictionary<TKey, TValue> or
  3. The non-generic IDictionary

Types implementing IDictionary use object to represent both key and value type shapes.

Construction Strategy

The construction strategy for dictionary types is inferred using the following priority:

  1. Types with public parameterless constructors that additionally expose Add and indexer methods, or implement one of the mutable IDictionary interfaces are classified as Mutable.
  2. Types annotated with CollectionBuilderAttribute are classified as Parameterized.
  3. Immutable or frozen dictionary types are classified as Parameterized.
  4. Types with public constructors accepting ReadOnlySpan<KeyValuePair<K,V>> or IEnumerable<KeyValuePair<K,V>> parameters are classified as Parameterized.
  5. Dictionary types not matching the above are classified as None.

PolyType will automatically select constructor or factory method overloads that additionally accept IEqualityComparer<Key> or IComparer<Key> and map those to the corresponding settings in the CollectionConstructionOptions<TKey> used by the constructor delegate. For parameterless constructors it will additionally look for int capacity parameters that map to the Capacity property.

Enumerable Types

A non-dictionary type is mapped to IEnumerableTypeShape when:

  1. It implements IEnumerable<T> (except string) or
  2. It implements non-generic IEnumerable (using object as the element type) or
  3. It implements IAsyncEnumerable<T> or
  4. It is an array type (including multi-dimensional arrays) or
  5. It is Memory<T> or ReadOnlyMemory<T>.

Construction Strategy

The construction strategy for enumerable types is inferred using the following priority:

  1. Types with public parameterless constructors that additionally expose Add methods, or implement one of the mutable ICollection interfaces are classified as Mutable.
  2. Types annotated with CollectionBuilderAttribute are classified as Parameterized.
  3. Immutable or frozen collection types are classified as Parameterized.
  4. Types with public constructors accepting ReadOnlySpan<TElement> or IEnumerable<TElement> parameters are classified as Parameterized.
  5. Enumerable types not matching the above are classified as None.

Object Types

Types not matching any of the above categories map to IObjectTypeShape. This includes primitive types and other irreducible values such as string, DateTimeOffset, or Guid which map to the type shape trivially without including any property or constructor shapes.

Property Shape Resolution

Property shapes are resolved using the following criteria:

  • Any public property or field is included as a property shape, unless explicitly ignored using a PropertyShapeAttribute.
  • Non-public members are not included, unless they have been explicitly annotated with a PropertyShapeAttribute.
  • Types annotated with DataContractAttribute only include members annotated with DataMemberAttribute or PropertyShapeAttribute.
  • Members from base types are included, with derived class members taking precedence over base class members with the same name (following the shadowing semantics of C#).
  • Members whose type does not support being a generic parameter (pointers, ref structs) are always skipped.

Read-only fields and init-only properties do not expose a setter delegate.

Constructor Shape Resolution

Constructor shapes are resolved using the following algorithm:

  • Only public constructors are considered by default.

  • Prefer constructors annotated with ConstructorShapeAttribute, even if non-public.

  • If the type defines multiple public constructors, pick one that:

    1. Minimizes the number of required parameters not corresponding to any property shapes, then
    2. Maximizes the number of parameters that match read-only properties/fields, and then
    3. Minimizes the total number of constructor parameters.

    Parameters correspond to a property shape if and only if they are of the same type and have matching names up to Pascal case/camel case equivalence.

If the selected constructor is parameterless and additionally there are no required or init-only properties defined on the type, it is mapped to an IConstructorShape that is parameterless and which relies on property shape setters to be populated.

Otherwise, the constructor gets mapped to a parameterized IConstructorShape. The logical signature of a parameterized constructor includes parameters and all settable members not corresponding to a constructor parameter, meaning that parameterized constructors DO NOT require additional binding from the object's property setter delegates.

Tuples

Tuple and value tuple types map to object shapes. Long tuple types, that is tuples with more than 7 elements are represented in IL using nested tuples. PolyType flattens long tuple representations so that all elements are accessible transparently from the outer tuple.

Method Shapes

Method shapes may be included in type shapes of any kind. By default, types do not include any method shapes and need to be opted in either by

The IncludeMethods approach only supports public methods, while MethodShapeAttribute can be used to opt in non-public methods.

Event Shapes

Event shapes may be included in type shapes of any kind. By default, types do not include any event shapes and need to be opted in either by