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 Status
{
    Active,
    Inactive,
    Pending
}
A Vogen value object property

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

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

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

C#
public class Person : Vertex
{
    public string? Name { get; set; }
    public Status Status { get; set; }
    public BirthDate BirthDate { 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 source, ITransformer defer, ITransformer recurse, [NotNullWhen(true)] out TTarget? value)
        {
            if (source.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 BirthDate-to-string serializer

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

C#
/// <summary>
/// Converts a BirthDate Vogen value object to a yyyy-MM-dd
/// string during serialization.
/// </summary>
public class BirthDateSerializerConverterFactory : IConverterFactory
{
    private sealed class BirthDateToStringConverter<TTarget>
        : IConverter<BirthDate, TTarget>
    {
        public bool TryConvert(
            BirthDate source,
            ITransformer defer,
            ITransformer recurse,
            [NotNullWhen(true)] out TTarget? value)
        {
            if (source.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(BirthDate) && typeof(TTarget).IsAssignableFrom(typeof(string)))
            return (IConverter<TSource, TTarget>?)Activator.CreateInstance(typeof(BirthDateToStringConverter<>).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 source, ITransformer defer, ITransformer recurse, out TTarget value)
        {
            if (source.Type == JTokenType.String && Enum.TryParse(source.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-BirthDate deserializer

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

C#
/// <summary>
/// Converts a yyyy-MM-dd string back to a BirthDate Vogen value
/// object during deserialization. Specific to Newtonsoft.Json (JValue input).
/// </summary>
public class BirthDateDeserializerConverterFactory : IConverterFactory
{
    private sealed class StringToBirthDateConverter<TTarget> : IConverter<JValue, TTarget>
    {
        public bool TryConvert(JValue source, ITransformer defer, ITransformer recurse, [NotNullWhen(true)] out TTarget? value)
        {
            if (source.Type == JTokenType.String && DateOnly.TryParseExact(source.Value<string>(), "yyyy-MM-dd", out var dateOnly) && BirthDate.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(BirthDate))
            return (IConverter<TSource, TTarget>?)Activator.CreateInstance(typeof(StringToBirthDateConverter<>).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 source = g
    .UseGremlinServer<Vertex, Edge>(_ => _
        .AtLocalhost())
    .ConfigureEnvironment(env => env
        .ConfigureNativeTypes(nativeTypes => nativeTypes
            .Add(typeof(Status))
            .Add(typeof(BirthDate)))
        .ConfigureSerializer(serializer => serializer
            .Add(new EnumToStringSerializerConverterFactory())
            .Add(new BirthDateSerializerConverterFactory()))
        .ConfigureDeserializer(deserializer => deserializer
            .Add(new StringToEnumDeserializerConverterFactory())
            .Add(new BirthDateDeserializerConverterFactory())));

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 source = g
    .UseGremlinServer<Vertex, Edge>(_ => _
        .AtLocalhost())
    .ConfigureEnvironment(env => env
        .ConfigureNativeTypes(nativeTypes => nativeTypes
            .Add(typeof(Status))
            .Add(typeof(BirthDate)))
        .ConfigureSerializer(serializer => serializer
            .Add(ConverterFactory.Create<Status, string>(
                (status, _, _, _) => status.ToString()))
            .Add(ConverterFactory.Create<BirthDate, string>(
                (birthDate, _, _, _) =>
                    birthDate.Value.ToString("yyyy-MM-dd"))))
        .ConfigureDeserializer(deserializer => deserializer
            .Add(ConverterFactory.Create<JValue, Status>(
                (jValue, _, _, _) =>
                    jValue.Type == JTokenType.String
                        ? Enum.Parse<Status>(
                            jValue.Value<string>()!)
                        : null))
            .Add(ConverterFactory.Create<JValue, BirthDate>(
                (jValue, _, _, _) =>
                    jValue.Type == JTokenType.String
                        ? BirthDate.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 source = g
    .UseGremlinServer<Vertex, Edge>(_ => _
        .AtLocalhost())
    .ConfigureEnvironment(env => env
        .RegisterNativeType<Status, string>(
            serializer: (status, _, _, _) =>
                status.ToString(),
            deserializer: (jValue, _, _, _) =>
                jValue.Type == JTokenType.String
                    ? Enum.Parse<Status>(jValue.Value<string>()!)
                    : default)
        .RegisterNativeType<BirthDate, string>(
            serializer: (birthDate, _, _, _) =>
                birthDate.Value.ToString("yyyy-MM-dd"),
            deserializer: (jValue, _, _, _) =>
                jValue.Type == JTokenType.String
                    ? BirthDate.From(DateOnly.ParseExact(
                        jValue.Value<string>()!,
                        "yyyy-MM-dd"))
                    : throw new InvalidOperationException(
                        $"Cannot deserialize {jValue} to BirthDate")));