Skip to content

Transformer

Gremlinq converts between .NET types and the graph database's wire format through a pair of transformer chains — one for serialization (.NET → database) and one for deserialization (database → .NET). Every custom type that is not natively understood by the database must be registered in these chains; this page describes how the mechanism works.

Concrete examples of registering custom types are in Custom types.

ITransformer — the chain

An ITransformer is an ordered collection of IConverterFactory instances. The two chains live on IGremlinQueryEnvironment as Serializer and Deserializer. You never create them directly; you extend them through ConfigureSerializer and ConfigureDeserializer.

C#
public interface ITransformer
{
    bool TryTransform<TSource, TTarget>(
        TSource source,
        IGremlinQueryEnvironment environment,
        [NotNullWhen(true)] out TTarget? value);

    ITransformer Add(IConverterFactory converterFactory);
}

Add returns a new ITransformer with the factory appended; the original is unchanged. Factories are tried in reverse registration order — the factory added last is tried first, so adding a factory always gives it higher priority than everything already registered.

IConverterFactory — negotiating the type pair

A factory is invoked once per unique (TSource, TTarget) type pair. Its job is to decide whether it can produce a converter for that pair. If it can, it returns an IConverter<TSource, TTarget>; if not, it returns null and the chain tries the previous factory.

C#
public interface IConverterFactory
{
    IConverter<TSource, TTarget>? TryCreate<TSource, TTarget>(IGremlinQueryEnvironment environment);
}

The environment parameter provides access to native types, options, and the rest of the query environment at the moment the converter is constructed. Inspect typeof(TSource) and typeof(TTarget) inside TryCreate to decide whether to handle the pair.

A factory that handles every enum type, for example, would return a converter when typeof(TSource).IsEnum is true and typeof(TTarget).IsAssignableFrom(typeof(string)) is true, and null for all other type pairs.

IConverter — performing the conversion

A converter handles one value of the negotiated type pair. The bool return signals success; out value carries the result and is meaningful only when true is returned.

C#
public interface IConverter<in TSource, TTarget>
{
    bool TryConvert(
        TSource source,
        ITransformer defer,
        ITransformer recurse,
        [NotNullWhen(true)] out TTarget? value);
}

The two transformer parameters allow calling back into the pipeline:

  • defer — contains only the factories registered before this one. Use it when you need to convert a sub-value without risk of re-entering your own converter and looping indefinitely.
  • recurse — contains the entire chain. Use it when you need to delegate conversion of a nested structure (e.g. the elements of a list) through the full pipeline.

Most simple scalar converters ignore both parameters and compute the result directly.

Configuration entry points

IGremlinQueryEnvironment exposes three methods for extending the pipeline:

C#
IGremlinQueryEnvironment ConfigureSerializer(
    Func<ITransformer, ITransformer> transformation);

IGremlinQueryEnvironment ConfigureDeserializer(
    Func<ITransformer, ITransformer> transformation);

IGremlinQueryEnvironment ConfigureNativeTypes(
    Func<IImmutableSet<Type>, IImmutableSet<Type>> transformation);

Each method is immutable — it returns a new IGremlinQueryEnvironment rather than modifying the existing one. The function you pass receives the current transformer and returns the modified one; the typical idiom is to call .Add(yourFactory) on it.

ConfigureNativeTypes maintains the set of types the environment treats as first-class scalar values. Without registering a type here, Gremlinq may try to decompose the value structurally — as though it were a vertex or edge — rather than routing it through the scalar converter chain. Always call ConfigureNativeTypes alongside ConfigureSerializer and ConfigureDeserializer when registering a new scalar type.

Resolution order and fallback

When TryTransform<TSource, TTarget> is called, the transformer walks the factory list from last to first. The first factory that returns a non-null converter for the type pair wins; that converter's TryConvert is then invoked. If TryConvert returns false, the chain continues to the next matching factory. If no factory produces a successful conversion, the transformer tries a direct cast (source is TTarget). If that also fails, it returns false.


Next: Custom types — registering enums, Vogen value objects, and other domain types using this infrastructure.