Table of Contents

Interface IShapeShiftConverterFactory<TEncoder, TDecoder>

Namespace
ShapeShift
Assembly
ShapeShift.dll

A factory for ShapeShiftConverter<TEncoder, TDecoder> objects of arbitrary types.

public interface IShapeShiftConverterFactory<TEncoder, TDecoder> where TEncoder : IEncoder, allows ref struct where TDecoder : IDecoder, allows ref struct

Type Parameters

TEncoder

The type of encoder to use.

TDecoder

The type of decoder to use.

Examples

A non-generic implementation of this interface is preferred when possible.

// A special purpose factory knows exactly what it supports, so no generic type parameter is needed.
public sealed class MoneyConverterFactory : IShapeShiftConverterFactory<JsonEncoder, JsonDecoder>
{
    public ShapeShiftConverter<JsonEncoder, JsonDecoder>? CreateConverter(Type type, ITypeShape? shape, in ConverterContext<JsonEncoder, JsonDecoder> context)
        => type == typeof(Money) ? new MoneyConverter() : null;
}

When a generic context is required, implement PolyType.Abstractions.ITypeShapeFunc on the same class and invoke into it after appropriate type checks.

// Writes the listed types as a JSON *string* holding their own JSON document, the shape some HTTP
// APIs require of an embedded payload.
public sealed class EmbeddedDocumentConverterFactory : IShapeShiftConverterFactory<JsonEncoder, JsonDecoder>, ITypeShapeFunc
{
    private readonly Type[] embeddedTypes;
public EmbeddedDocumentConverterFactory(params Type[] embeddedTypes) => this.embeddedTypes = embeddedTypes;

// The type check needs no generic type parameter, so it happens here; the converter does need
// one, so this method hands the shape back to the generic method below.
public ShapeShiftConverter<JsonEncoder, JsonDecoder>? CreateConverter(Type type, ITypeShape? shape, in ConverterContext<JsonEncoder, JsonDecoder> context)
    => shape is not null && Array.IndexOf(this.embeddedTypes, type) >= 0
        ? (ShapeShiftConverter<JsonEncoder, JsonDecoder>?)shape.Invoke(this)
        : null;

// The type check is already done, so just create the converter. ITypeShape<T>.Invoke supplies
// the generic type parameter without reflection, which keeps this factory NativeAOT-safe.
object? ITypeShapeFunc.Invoke<T>(ITypeShape<T> typeShape, object? state)
    => new EmbeddedDocumentConverter<T>(typeShape);

}

public sealed class EmbeddedDocumentConverter<T> : ShapeShiftConverter<T, JsonEncoder, JsonDecoder> { // The embedded document is written by an ordinary serializer of its own, which is how it gets // its own policies (and why it never recurses back into the outer serializer's factories). private static readonly JsonSerializer Embedded = new();

private readonly ITypeShape<T> shape;

public EmbeddedDocumentConverter(ITypeShape<T> shape) => this.shape = shape;

public override T? Read(ref JsonDecoder decoder, SerializationContext<JsonEncoder, JsonDecoder> context)
{
    if (decoder.TryReadNull())
    {
        return default;
    }

    context.DepthStep();
    byte[] utf8 = Encoding.UTF8.GetBytes(decoder.ReadString());
    JsonDecoder embedded = new(utf8);
    return Embedded.Deserialize(ref embedded, this.shape, context.CancellationToken);
}

public override void Write(ref JsonEncoder encoder, in T? value, SerializationContext<JsonEncoder, JsonDecoder> context)
{
    context.DepthStep();
    ArrayBufferWriter<byte> buffer = new();
    using (System.Text.Json.Utf8JsonWriter writer = new(buffer))
    {
        JsonEncoder embedded = new(writer);
        Embedded.Serialize(ref embedded, value, this.shape, context.CancellationToken);
    }

    encoder.WriteValue(Encoding.UTF8.GetString(buffer.WrittenSpan));
}

}

When generic type parameters are required for sub-elements of the type to be converted (e.g. the element type of a collection), you can leverage a PolyType.Abstractions.TypeShapeVisitor implementation to obtain the generic type parameters.

// Reads a null JSON array as an empty list, which lets a service accept peers that write null for
// an absent collection without making every model property nullable.
public sealed class NullTolerantListConverterFactory : IShapeShiftConverterFactory<JsonEncoder, JsonDecoder>
{
    // The converter needs the *element* type as a generic type parameter, which a TypeShapeVisitor
    // supplies. Perform the type check, then defer to the visitor.
    public ShapeShiftConverter<JsonEncoder, JsonDecoder>? CreateConverter(Type type, ITypeShape? shape, in ConverterContext<JsonEncoder, JsonDecoder> context)
        => shape is IEnumerableTypeShape && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)
            ? (ShapeShiftConverter<JsonEncoder, JsonDecoder>?)shape.Accept(Visitor.Instance, context)
            : null;
private sealed class Visitor : TypeShapeVisitor
{
    internal static readonly Visitor Instance = new();

    public override object? VisitEnumerable<TEnumerable, TElement>(IEnumerableTypeShape<TEnumerable, TElement> enumerableShape, object? state = null)
    {
        var context = (ConverterContext<JsonEncoder, JsonDecoder>)state!;
        return new NullTolerantListConverter<TElement>(context.GetConverter(enumerableShape.ElementType));
    }
}

}

public sealed class NullTolerantListConverter<TElement> : ShapeShiftConverter<List<TElement>, JsonEncoder, JsonDecoder> { private readonly ShapeShiftConverter<TElement, JsonEncoder, JsonDecoder> elementConverter;

public NullTolerantListConverter(ShapeShiftConverter<TElement, JsonEncoder, JsonDecoder> elementConverter)
    => this.elementConverter = elementConverter;

public override List<TElement> Read(ref JsonDecoder decoder, SerializationContext<JsonEncoder, JsonDecoder> context)
{
    if (decoder.TryReadNull())
    {
        return [];
    }

    context.DepthStep();
    List<TElement> elements = decoder.ReadStartVector() is int count ? new(count) : [];
    while (decoder.NextTokenType != TokenType.EndVector)
    {
        // A custom converter is responsible for honoring the context's security limits.
        if (elements.Count == context.MaxCollectionLength)
        {
            throw new ShapeShiftSerializationException($"Collection length exceeds the configured maximum of {context.MaxCollectionLength}.");
        }

        elements.Add(this.elementConverter.Read(ref decoder, context)!);
    }

    decoder.ReadEndVector();
    return elements;
}

public override void Write(ref JsonEncoder encoder, in List<TElement>? value, SerializationContext<JsonEncoder, JsonDecoder> context)
{
    context.DepthStep();
    List<TElement> elements = value ?? [];
    encoder.WriteStartVector(elements.Count);
    foreach (TElement element in elements)
    {
        this.elementConverter.Write(ref encoder, element, context);
    }

    encoder.WriteEndVector();
}

}

Methods

CreateConverter(Type, ITypeShape?, in ConverterContext<TEncoder, TDecoder>)

Creates a converter for the given type if this factory is capable of it.

ShapeShiftConverter<TEncoder, TDecoder>? CreateConverter(Type type, ITypeShape? shape, in ConverterContext<TEncoder, TDecoder> context)

Parameters

type Type

The type to be serialized.

shape ITypeShape

The shape of the type to be serialized, if available. The shape will typically be available. The only known exception is when the type has a PolyType marshaler defined.

context ConverterContext<TEncoder, TDecoder>

The context in which this factory is being invoked. Provides access to other converters that may be required by the requested converter.

Returns

ShapeShiftConverter<TEncoder, TDecoder>

The converter for the data type, or null.

Remarks

Implementations that require a generic type parameter for the type to be converted should also implement PolyType.Abstractions.ITypeShapeFunc with an Invoke<T>(ITypeShape<T>, object) method that creates the converter. The implementation of this method should perform any type checks necessary to determine whether this factory applies to the given shape, and if so, call Invoke<T>(ITypeShape<T>, object) on the shape, passing in this to forward the call to the generic Invoke<T>(ITypeShape<T>, object) method defined on that same class.