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
TEncoderThe type of encoder to use. TDecoderThe 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
typeTypeThe type to be serialized.
shapeITypeShapeThe 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.
contextConverterContext<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.