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, returningtrueon 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.
A Vogen value object property
Vogen generates strongly-typed wrappers around primitive
types. Here a BirthDate wraps a DateOnly:
Using custom types in an entity
Custom types are used on entities exactly like built-in types:
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.
/// <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.
/// <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.
/// <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.
/// <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:
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:
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.
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")));