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:
- Object - IObjectTypeShape for general object types with properties and constructors.
- Enumerable - IEnumerableTypeShape for collection types implementing IEnumerable<T>
- Dictionary - IDictionaryTypeShape for key-value collection types.
- Enum - IEnumTypeShape for enum types.
- Optional - IOptionalTypeShape for nullable value types and F# options.
- Surrogate - ISurrogateTypeShape for types that define a marshaller to a surrogate type.
- Union - IUnionTypeShape for polymorphic type hierarchies or discriminated union types.
- Function - IFunctionTypeShape for delegate and F# function types.
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:
- It is Nullable<T> (e.g.,
int?
,DateTime?
) or - It is an F# option type
Function Types
A type is mapped to IFunctionTypeShape when:
- It is a delegate type or
- 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:
- It is a class with DerivedTypeShapeAttribute annotations or
- It has
DataContractAttribute
withKnownTypeAttribute
annotations or - It is an F# union type.
Dictionary Types
A type is mapped to IDictionaryTypeShape when it implements:
- IDictionary<TKey, TValue> or
- IReadOnlyDictionary<TKey, TValue> or
- 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:
- Types with public parameterless constructors that additionally expose
Add
and indexer methods, or implement one of the mutableIDictionary
interfaces are classified as Mutable. - Types annotated with
CollectionBuilderAttribute
are classified as Parameterized. - Immutable or frozen dictionary types are classified as Parameterized.
- Types with public constructors accepting
ReadOnlySpan<KeyValuePair<K,V>>
orIEnumerable<KeyValuePair<K,V>>
parameters are classified as Parameterized. - 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:
- It implements IEnumerable<T> (except string) or
- It implements non-generic IEnumerable (using
object
as the element type) or - It implements
IAsyncEnumerable<T>
or - It is an array type (including multi-dimensional arrays) or
- It is Memory<T> or ReadOnlyMemory<T>.
Construction Strategy
The construction strategy for enumerable types is inferred using the following priority:
- Types with public parameterless constructors that additionally expose
Add
methods, or implement one of the mutableICollection
interfaces are classified as Mutable. - Types annotated with
CollectionBuilderAttribute
are classified as Parameterized. - Immutable or frozen collection types are classified as Parameterized.
- Types with public constructors accepting
ReadOnlySpan<TElement>
orIEnumerable<TElement>
parameters are classified as Parameterized. - 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 withDataMemberAttribute
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:
- Minimizes the number of required parameters not corresponding to any property shapes, then
- Maximizes the number of parameters that match read-only properties/fields, and then
- 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
- Configuring the
IncludeMethods
property in either of the TypeShapeAttribute, GenerateShapeAttribute, or TypeShapeExtensionAttribute or - Explicitly annotating a method with the MethodShapeAttribute.
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
- Configuring the
IncludeMethods
property in either of the TypeShapeAttribute, GenerateShapeAttribute, or TypeShapeExtensionAttribute or - Explicitly annotating an event with the EventShapeAttribute.