Skip to content

Custom types

When a vertex or edge property uses a type that the graph database does not natively understand — such as an enum, a Vogen value object, or any other domain type — you need to tell Gremlinq how to convert that type to and from a database-friendly representation.

Gremlinq's conversion system is built on two core interfaces from ExRam.Gremlinq.Core.Transformation:

  • IConverterFactory — creates converters for a given source/target type pair.
  • IConverter<TSource, TTarget> — performs the actual conversion, returning true on success.

Converter factories are registered on the query environment via ConfigureSerializer (your type → database representation) and ConfigureDeserializer (database representation → your type). Types that the database should treat as first-class scalars are also registered via ConfigureNativeTypes.

Defining entities with custom types

An enum property

Define an enum that represents the domain concept, then use it on a vertex or edge property just like any other scalar.

C#
public enum AirportStatus
{
    Open,
    Closed,
    Limited
}
A Vogen value object property

Vogen generates strongly-typed wrappers around primitive types. Here an OpenedDate wraps a DateOnly:

C#
[ValueObject<DateOnly>]
public partial struct OpenedDate
{
}
Using custom types in an entity

Custom types are used on entities exactly like built-in types:

C#
public class Airport : Vertex
{
    public string? Code { get; set; }
    public string? ICAO { get; set; }
    public string? City { get; set; }
    public string? Country { get; set; }
    public int Elevation { get; set; }
    public AirportStatus Status { get; set; }
    public OpenedDate OpenedDate { get; set; }
}

Serialization converter factories

Serialization converter factories convert your custom .NET types into values the database understands. They are registered with ConfigureSerializer.

A generic enum-to-string serializer

This factory handles every enum type in the model. When the serializer asks for a conversion from any enum to string, the factory creates a converter that calls ToString() on the enum value.

The TryCreate method inspects the requested type pair at runtime and returns a converter only when TSource is an enum and TTarget is assignable from string. Otherwise it returns null, letting the next factory in the chain handle the request.

C#
/// <summary>
/// Converts any enum to its string name during serialization.
/// A single factory handles every enum type in the model.
/// </summary>
public class EnumToStringSerializerConverterFactory : IConverterFactory
{
    private sealed class EnumToStringConverter<TEnum, TTarget>  : IConverter<TEnum, TTarget>
        where TEnum : struct, Enum
    {
        public bool TryConvert(TEnum _g, ITransformer defer, ITransformer recurse, [NotNullWhen(true)] out TTarget? value)
        {
            if (_g.ToString() is TTarget target)
            {
                value = target;
                return true;
            }

            value = default;
            return false;
        }
    }

    public IConverter<TSource, TTarget>? TryCreate<TSource, TTarget>(IGremlinQueryEnvironment environment)
    {
        if (typeof(TSource).IsEnum && typeof(TTarget).IsAssignableFrom(typeof(string)))
            return (IConverter<TSource, TTarget>?)Activator.CreateInstance(typeof(EnumToStringConverter<,>).MakeGenericType(typeof(TSource), typeof(TTarget)));

        return null;
    }
}
A OpenedDate-to-string serializer

This factory is specific to a single type. It converts an OpenedDate Vogen value object into a yyyy-MM-dd formatted string by accessing the underlying DateOnly via .Value.

C#
/// <summary>
/// Converts an OpenedDate Vogen value object to a yyyy-MM-dd
/// string during serialization.
/// </summary>
public class OpenedDateSerializerConverterFactory : IConverterFactory
{
    private sealed class OpenedDateToStringConverter<TTarget> : IConverter<OpenedDate, TTarget>
    {
        public bool TryConvert(
            OpenedDate _g,
            ITransformer defer,
            ITransformer recurse,
            [NotNullWhen(true)] out TTarget? value)
        {
            if (_g.Value.ToString("yyyy-MM-dd") is TTarget target)
            {
                value = target;
                return true;
            }

            value = default;
            return false;
        }
    }

    public IConverter<TSource, TTarget>? TryCreate<TSource, TTarget>(IGremlinQueryEnvironment environment)
    {
        if (typeof(TSource) == typeof(OpenedDate) && typeof(TTarget).IsAssignableFrom(typeof(string)))
            return (IConverter<TSource, TTarget>?)Activator.CreateInstance(typeof(OpenedDateToStringConverter<>).MakeGenericType(typeof(TTarget)));

        return null;
    }
}

Deserialization converter factories

Deserialization converter factories convert database values back into your custom .NET types. They are registered with ConfigureDeserializer.

Source type depends on the serialization support package

The examples below use JValue (from Newtonsoft.Json) as the source type because the project uses ExRam.Gremlinq.Support.NewtonsoftJson. With a different support package (e.g. System.Text.Json), the source type would differ accordingly.

A generic string-to-enum deserializer

The counterpart to the serializer above. When the deserializer asks for a conversion from JValue to any enum, the factory creates a converter that parses the string value back into the enum.

C#
/// <summary>
/// Converts a string back to an enum during deserialization.
/// Handles any enum type. Specific to Newtonsoft.Json (JValue input).
/// </summary>
public class StringToEnumDeserializerConverterFactory : IConverterFactory
{
    private sealed class StringToEnumConverter<TTarget> : IConverter<JValue, TTarget>
        where TTarget : struct, Enum
    {
        public bool TryConvert(JValue _g, ITransformer defer, ITransformer recurse, out TTarget value)
        {
            if (_g.Type == JTokenType.String && Enum.TryParse(_g.Value<string>(), out value))
                return true;

            value = default;
            return false;
        }
    }

    public IConverter<TSource, TTarget>? TryCreate<TSource, TTarget>(IGremlinQueryEnvironment environment)
    {
        if (typeof(TSource) == typeof(JValue) && typeof(TTarget).IsEnum)
            return (IConverter<TSource, TTarget>?)Activator.CreateInstance(typeof(StringToEnumConverter<>).MakeGenericType(typeof(TTarget)));

        return null;
    }
}
A string-to-OpenedDate deserializer

Parses a yyyy-MM-dd string from the database back into an OpenedDate value object.

C#
/// <summary>
/// Converts a yyyy-MM-dd string back to an OpenedDate Vogen value
/// object during deserialization. Specific to Newtonsoft.Json (JValue input).
/// </summary>
public class OpenedDateDeserializerConverterFactory : IConverterFactory
{
    private sealed class StringToOpenedDateConverter<TTarget> : IConverter<JValue, TTarget>
    {
        public bool TryConvert(JValue _g, ITransformer defer, ITransformer recurse, [NotNullWhen(true)] out TTarget? value)
        {
            if (_g.Type == JTokenType.String && DateOnly.TryParseExact(_g.Value<string>(), "yyyy-MM-dd", out var dateOnly) && OpenedDate.From(dateOnly) is TTarget target)
            {
                value = target;
                return true;
            }

            value = default;
            return false;
        }
    }

    public IConverter<TSource, TTarget>? TryCreate<TSource, TTarget>(IGremlinQueryEnvironment environment)
    {
        if (typeof(TSource) == typeof(JValue) && typeof(TTarget) == typeof(OpenedDate))
            return (IConverter<TSource, TTarget>?)Activator.CreateInstance(typeof(StringToOpenedDateConverter<>).MakeGenericType(typeof(TTarget)));

        return null;
    }
}

Wiring converter factories

ConfigureNativeTypes, ConfigureSerializer and ConfigureDeserializer

Register the custom types as native types so Gremlinq treats them as scalars, then add the serialization and deserialization factories to the respective transformer chains:

C#
var _g = g
    .UseGremlinServer<Vertex, Edge>(_ => _
        .AtLocalhost())
    .ConfigureEnvironment(env => env
        .ConfigureNativeTypes(nativeTypes => nativeTypes
            .Add(typeof(AirportStatus))
            .Add(typeof(OpenedDate)))
        .ConfigureSerializer(serializer => serializer
            .Add(new EnumToStringSerializerConverterFactory())
            .Add(new OpenedDateSerializerConverterFactory()))
        .ConfigureDeserializer(deserializer => deserializer
            .Add(new StringToEnumDeserializerConverterFactory())
            .Add(new OpenedDateDeserializerConverterFactory())));

Simpler alternatives

For straightforward scalar-to-scalar conversions, Gremlinq provides convenience shortcuts that avoid writing full IConverterFactory classes.

Using ConverterFactory.Create

ConverterFactory.Create<TSource, TTarget>(...) wraps a lambda into an IConverterFactory. You still call ConfigureNativeTypes, ConfigureSerializer, and ConfigureDeserializer yourself, but without having to write converter classes:

C#
var _g = g
    .UseGremlinServer<Vertex, Edge>(_ => _
        .AtLocalhost())
    .ConfigureEnvironment(env => env
        .ConfigureNativeTypes(nativeTypes => nativeTypes
            .Add(typeof(AirportStatus))
            .Add(typeof(OpenedDate)))
        .ConfigureSerializer(serializer => serializer
            .Add(ConverterFactory.Create<AirportStatus, string>(
                (status, _, _, _) => status.ToString()))
            .Add(ConverterFactory.Create<OpenedDate, string>(
                (openedDate, _, _, _) =>
                    openedDate.Value.ToString("yyyy-MM-dd"))))
        .ConfigureDeserializer(deserializer => deserializer
            .Add(ConverterFactory.Create<JValue, AirportStatus>(
                (jValue, _, _, _) =>
                    jValue.Type == JTokenType.String
                        ? Enum.Parse<AirportStatus>(
                            jValue.Value<string>()!)
                        : null))
            .Add(ConverterFactory.Create<JValue, OpenedDate>(
                (jValue, _, _, _) =>
                    jValue.Type == JTokenType.String
                        ? OpenedDate.From(DateOnly.ParseExact(
                            jValue.Value<string>()!,
                            "yyyy-MM-dd"))
                        : null))));
Using RegisterNativeType (Newtonsoft.Json only)

RegisterNativeType is a convenience method on IGremlinQueryEnvironment provided by ExRam.Gremlinq.Support.NewtonsoftJson. It combines ConfigureNativeTypes, ConfigureSerializer and ConfigureDeserializer in a single call.

Because the deserializer side is hard-coded to JValue, this method is only available when using the Newtonsoft.Json support package.

C#
var _g = g
    .UseGremlinServer<Vertex, Edge>(_ => _
        .AtLocalhost())
    .ConfigureEnvironment(env => env
        .RegisterNativeType(
            serializer: (status, _, _, _) =>
                status.ToString(),
            deserializer: (jValue, _, _, _) =>
                jValue.Type == JTokenType.String
                    ? Enum.Parse<AirportStatus>(jValue.Value<string>()!)
                    : default)
        .RegisterNativeType(
            serializer: (openedDate, _, _, _) =>
                openedDate.Value.ToString("yyyy-MM-dd"),
            deserializer: (jValue, _, _, _) =>
                jValue.Type == JTokenType.String
                    ? OpenedDate.From(DateOnly.ParseExact(
                        jValue.Value<string>()!,
                        "yyyy-MM-dd"))
                    : throw new InvalidOperationException(
                        $"Cannot deserialize {jValue} to OpenedDate")));

See also: TransformerITransformer, IConverterFactory, and IConverter explained  |  Vertex Properties — TinkerPop vertex properties with meta-properties  |  Property Metadata — renaming properties and controlling write behaviour.