You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tinkerpop.apache.org by fl...@apache.org on 2021/08/27 14:01:13 UTC
[tinkerpop] 01/01: TINKERPOP-2570 Support custom types in
GraphBinary for .NET
This is an automated email from the ASF dual-hosted git repository.
florianhockmann pushed a commit to branch TINKERPOP-2570
in repository https://gitbox.apache.org/repos/asf/tinkerpop.git
commit effcf07c94bd73b718c8f25d4e747b9f8ca90a1d
Author: Florian Hockmann <fh...@florian-hockmann.de>
AuthorDate: Fri Aug 27 15:58:30 2021 +0200
TINKERPOP-2570 Support custom types in GraphBinary for .NET
---
CHANGELOG.asciidoc | 1 +
.../Structure/IO/GraphBinary/DataType.cs | 5 +
.../IO/GraphBinary/GraphBinaryMessageSerializer.cs | 18 +++-
.../Structure/IO/GraphBinary/GraphBinaryReader.cs | 25 ++++-
.../Structure/IO/GraphBinary/GraphBinaryWriter.cs | 22 +++-
.../IO/GraphBinary/TypeSerializerRegistry.cs | 112 +++++++++++++++++---
.../IO/GraphBinary/Types/CustomTypeSerializer.cs | 54 ++++++++++
.../IO/GraphBinary/Types/Sample/SamplePerson.cs | 59 +++++++++++
.../Types/Sample/SamplePersonSerializer.cs | 117 +++++++++++++++++++++
.../Types/Sample/SamplePersonSerializerTests.cs | 77 ++++++++++++++
10 files changed, 471 insertions(+), 19 deletions(-)
diff --git a/CHANGELOG.asciidoc b/CHANGELOG.asciidoc
index c60f7af..1da7a6b 100644
--- a/CHANGELOG.asciidoc
+++ b/CHANGELOG.asciidoc
@@ -47,6 +47,7 @@ image::https://raw.githubusercontent.com/apache/tinkerpop/master/docs/static/ima
* Ensured `PathRetractionStrategy` is applied after `InlineFilterStrategy` which prevents an error in traverser mapping in certain conditions.
* Deprecated `JsonBuilder` serialization for GraphSON and Gryo.
* Created `GremlinParser` to construct `Traversal` objects from `gremlin-language`.
+* Added support for custom GraphBinary types in .NET.
[[release-3-5-1]]
=== TinkerPop 3.5.1 (Release Date: July 19, 2021)
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/DataType.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/DataType.cs
index cb309c2..3e65ad0 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/DataType.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/DataType.cs
@@ -79,6 +79,11 @@ namespace Gremlin.Net.Structure.IO.GraphBinary
#pragma warning restore 1591
/// <summary>
+ /// A custom type, represented as a blob value.
+ /// </summary>
+ public static readonly DataType Custom = new DataType(0);
+
+ /// <summary>
/// A null value for an unspecified Object value.
/// </summary>
public static readonly DataType UnspecifiedNull = new DataType(0xFE);
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/GraphBinaryMessageSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/GraphBinaryMessageSerializer.cs
index 2b56935..97c38cf 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/GraphBinaryMessageSerializer.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/GraphBinaryMessageSerializer.cs
@@ -21,6 +21,7 @@
#endregion
+using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
@@ -46,12 +47,23 @@ namespace Gremlin.Net.Structure.IO.GraphBinary
/// <summary>
/// Initializes a new instance of the <see cref="GraphBinaryMessageSerializer" /> class.
/// </summary>
+ /// <param name="registry">The <see cref="TypeSerializerRegistry"/> to use for serialization.</param>
+ public GraphBinaryMessageSerializer(TypeSerializerRegistry registry = null)
+ {
+ _reader = new GraphBinaryReader(registry);
+ _writer = new GraphBinaryWriter(registry);
+ }
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GraphBinaryMessageSerializer" /> class.
+ /// </summary>
/// <param name="reader">The <see cref="GraphBinaryReader"/> used to deserialize from GraphBinary.</param>
/// <param name="writer">The <see cref="GraphBinaryWriter"/> used to serialize to GraphBinary.</param>
- public GraphBinaryMessageSerializer(GraphBinaryReader reader = null, GraphBinaryWriter writer = null)
+ [Obsolete("Use the constructor that takes a TypeSerializerRegistry instead.")]
+ public GraphBinaryMessageSerializer(GraphBinaryReader reader, GraphBinaryWriter writer)
{
- _reader = reader ?? new GraphBinaryReader();
- _writer = writer ?? new GraphBinaryWriter();
+ _reader = reader;
+ _writer = writer;
}
/// <inheritdoc />
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/GraphBinaryReader.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/GraphBinaryReader.cs
index deaf31a..3d030ef 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/GraphBinaryReader.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/GraphBinaryReader.cs
@@ -31,7 +31,16 @@ namespace Gremlin.Net.Structure.IO.GraphBinary
/// </summary>
public class GraphBinaryReader
{
- private readonly TypeSerializerRegistry _registry = new TypeSerializerRegistry();
+ private readonly TypeSerializerRegistry _registry;
+
+ /// <summary>
+ /// Initializes a new instance of the <see cref="GraphBinaryReader" /> class.
+ /// </summary>
+ /// <param name="registry">The <see cref="TypeSerializerRegistry"/> to use for deserialization.</param>
+ public GraphBinaryReader(TypeSerializerRegistry registry = null)
+ {
+ _registry = registry ?? TypeSerializerRegistry.Instance;
+ }
/// <summary>
/// Reads only the value for a specific type <typeparamref name="T"/>.
@@ -61,8 +70,18 @@ namespace Gremlin.Net.Structure.IO.GraphBinary
return default; // should be null (TODO?)
}
- var typedSerializer = _registry.GetSerializerFor(type);
- return await typedSerializer.ReadAsync(stream, this).ConfigureAwait(false);
+ ITypeSerializer typeSerializer;
+ if (type != DataType.Custom)
+ {
+ typeSerializer = _registry.GetSerializerFor(type);
+ }
+ else
+ {
+ var customTypeName = (string)await ReadValueAsync<string>(stream, false).ConfigureAwait(false);
+ typeSerializer = _registry.GetSerializerForCustomType(customTypeName);
+ }
+
+ return await typeSerializer.ReadAsync(stream, this).ConfigureAwait(false);
}
}
}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/GraphBinaryWriter.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/GraphBinaryWriter.cs
index e1eec8f..2dec83f 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/GraphBinaryWriter.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/GraphBinaryWriter.cs
@@ -23,6 +23,7 @@
using System.IO;
using System.Threading.Tasks;
+using Gremlin.Net.Structure.IO.GraphBinary.Types;
namespace Gremlin.Net.Structure.IO.GraphBinary
{
@@ -40,10 +41,20 @@ namespace Gremlin.Net.Structure.IO.GraphBinary
public const byte VersionByte = 0x81;
private static readonly byte[] UnspecifiedNullBytes = {DataType.UnspecifiedNull.TypeCode, 0x01};
+ private static readonly byte[] CustomTypeCodeBytes = { DataType.Custom.TypeCode };
- private readonly TypeSerializerRegistry _registry = new TypeSerializerRegistry();
+ private readonly TypeSerializerRegistry _registry;
/// <summary>
+ /// Initializes a new instance of the <see cref="GraphBinaryWriter" /> class.
+ /// </summary>
+ /// <param name="registry">The <see cref="TypeSerializerRegistry"/> to use for serialization.</param>
+ public GraphBinaryWriter(TypeSerializerRegistry registry = null)
+ {
+ _registry = registry ?? TypeSerializerRegistry.Instance;
+ }
+
+ /// <summary>
/// Writes a value without including type information.
/// </summary>
/// <param name="value">The value to write.</param>
@@ -84,6 +95,15 @@ namespace Gremlin.Net.Structure.IO.GraphBinary
var valueType = value.GetType();
var serializer = _registry.GetSerializerFor(valueType);
+
+ if (serializer is CustomTypeSerializer customTypeSerializer)
+ {
+ await stream.WriteAsync(CustomTypeCodeBytes).ConfigureAwait(false);
+ await WriteValueAsync(customTypeSerializer.TypeName, stream, false).ConfigureAwait(false);
+ await customTypeSerializer.WriteAsync(value, stream, this).ConfigureAwait(false);
+ return;
+ }
+
await stream.WriteByteAsync(serializer.DataType.TypeCode).ConfigureAwait(false);
await serializer.WriteAsync(value, stream, this).ConfigureAwait(false);
}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/TypeSerializerRegistry.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/TypeSerializerRegistry.cs
index efa1699..a713ca8 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/TypeSerializerRegistry.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/TypeSerializerRegistry.cs
@@ -36,7 +36,7 @@ namespace Gremlin.Net.Structure.IO.GraphBinary
/// </summary>
public class TypeSerializerRegistry
{
- private static readonly Dictionary<Type, ITypeSerializer> SerializerByType =
+ private readonly Dictionary<Type, ITypeSerializer> _serializerByType =
new Dictionary<Type, ITypeSerializer>
{
{typeof(int), SingleTypeSerializers.IntSerializer},
@@ -81,7 +81,7 @@ namespace Gremlin.Net.Structure.IO.GraphBinary
{typeof(TimeSpan), new DurationSerializer()},
};
- private static readonly Dictionary<DataType, ITypeSerializer> SerializerByDataType =
+ private readonly Dictionary<DataType, ITypeSerializer> _serializerByDataType =
new Dictionary<DataType, ITypeSerializer>
{
{DataType.Int, SingleTypeSerializers.IntSerializer},
@@ -129,6 +129,36 @@ namespace Gremlin.Net.Structure.IO.GraphBinary
{DataType.Duration, new DurationSerializer()},
};
+ private readonly Dictionary<string, CustomTypeSerializer> _serializerByCustomTypeName =
+ new Dictionary<string, CustomTypeSerializer>();
+
+ private TypeSerializerRegistry(List<CustomTypeRegistryEntry> entries)
+ {
+ foreach (var entry in entries)
+ {
+ _serializerByType[entry.Type] = entry.TypeSerializer;
+
+ if (entry.DataType == DataType.Custom)
+ {
+ _serializerByCustomTypeName[entry.CustomTypeName] = entry.TypeSerializer;
+ }
+ else
+ {
+ _serializerByDataType[entry.DataType] = entry.TypeSerializer;
+ }
+ }
+ }
+
+ /// <summary>
+ /// Provides a default <see cref="TypeSerializerRegistry"/> instance.
+ /// </summary>
+ public static readonly TypeSerializerRegistry Instance = Build().Create();
+
+ /// <summary>
+ /// Builds a <see cref="TypeSerializerRegistry"/>.
+ /// </summary>
+ public static Builder Build() => new Builder();
+
/// <summary>
/// Gets a serializer for the given type of the value to be serialized.
/// </summary>
@@ -137,9 +167,9 @@ namespace Gremlin.Net.Structure.IO.GraphBinary
/// <exception cref="InvalidOperationException">Thrown when no serializer can be found for the type.</exception>
public ITypeSerializer GetSerializerFor(Type valueType)
{
- if (SerializerByType.ContainsKey(valueType))
+ if (_serializerByType.ContainsKey(valueType))
{
- return SerializerByType[valueType];
+ return _serializerByType[valueType];
}
if (IsDictionaryType(valueType))
@@ -148,7 +178,7 @@ namespace Gremlin.Net.Structure.IO.GraphBinary
var dictValueType = valueType.GetGenericArguments()[1];
var serializerType = typeof(MapSerializer<,>).MakeGenericType(dictKeyType, dictValueType);
var serializer = (ITypeSerializer) Activator.CreateInstance(serializerType);
- SerializerByType[valueType] = serializer;
+ _serializerByType[valueType] = serializer;
return serializer;
}
@@ -157,7 +187,7 @@ namespace Gremlin.Net.Structure.IO.GraphBinary
var memberType = valueType.GetGenericArguments()[0];
var serializerType = typeof(SetSerializer<,>).MakeGenericType(valueType, memberType);
var serializer = (ITypeSerializer) Activator.CreateInstance(serializerType);
- SerializerByType[valueType] = serializer;
+ _serializerByType[valueType] = serializer;
return serializer;
}
@@ -166,7 +196,7 @@ namespace Gremlin.Net.Structure.IO.GraphBinary
var memberType = valueType.GetElementType();
var serializerType = typeof(ArraySerializer<>).MakeGenericType(memberType);
var serializer = (ITypeSerializer) Activator.CreateInstance(serializerType);
- SerializerByType[valueType] = serializer;
+ _serializerByType[valueType] = serializer;
return serializer;
}
@@ -175,16 +205,16 @@ namespace Gremlin.Net.Structure.IO.GraphBinary
var memberType = valueType.GetGenericArguments()[0];
var serializerType = typeof(ListSerializer<>).MakeGenericType(memberType);
var serializer = (ITypeSerializer) Activator.CreateInstance(serializerType);
- SerializerByType[valueType] = serializer;
+ _serializerByType[valueType] = serializer;
return serializer;
}
- foreach (var supportedType in SerializerByType.Keys)
+ foreach (var supportedType in _serializerByType.Keys)
{
if (supportedType.IsAssignableFrom(valueType))
{
- var serializer = SerializerByType[supportedType];
- SerializerByType[valueType] = serializer;
+ var serializer = _serializerByType[supportedType];
+ _serializerByType[valueType] = serializer;
return serializer;
}
}
@@ -218,7 +248,65 @@ namespace Gremlin.Net.Structure.IO.GraphBinary
/// <returns>A serializer for the provided GraphBinary type.</returns>
public ITypeSerializer GetSerializerFor(DataType type)
{
- return SerializerByDataType[type];
+ if (type == DataType.Custom)
+ throw new ArgumentException("Custom type serializers can not be retrieved using this method");
+
+ return _serializerByDataType[type];
+ }
+
+ /// <summary>
+ /// Gets a serializer for the given custom type name.
+ /// </summary>
+ /// <param name="typeName">The custom type name.</param>
+ /// <returns>A serializer for the provided custom type name.</returns>
+ public CustomTypeSerializer GetSerializerForCustomType(string typeName)
+ {
+ return _serializerByCustomTypeName[typeName];
+ }
+
+ /// <summary>
+ /// The builder of a <see cref="TypeSerializerRegistry"/>.
+ /// </summary>
+ public class Builder
+ {
+ private readonly List<CustomTypeRegistryEntry> _list = new List<CustomTypeRegistryEntry>();
+
+ /// <summary>
+ /// Creates the <see cref="TypeSerializerRegistry"/>.
+ /// </summary>
+ public TypeSerializerRegistry Create() => new TypeSerializerRegistry(_list);
+
+ /// <summary>
+ /// Adds a serializer for a custom type.
+ /// </summary>
+ /// <param name="type">The custom type supported by the serializer.</param>
+ /// <param name="serializer">The serializer for the custom type.</param>
+ public Builder AddCustomType(Type type, CustomTypeSerializer serializer)
+ {
+ if (serializer == null) throw new ArgumentNullException(nameof(serializer));
+ if (serializer.TypeName == null)
+ throw new ArgumentException("serializer custom type name can not be null");
+
+ _list.Add(new CustomTypeRegistryEntry(type, serializer));
+ return this;
+ }
+ }
+
+ private class CustomTypeRegistryEntry
+ {
+ public Type Type { get; }
+ public CustomTypeSerializer TypeSerializer { get; }
+
+ public CustomTypeRegistryEntry(Type type, CustomTypeSerializer typeSerializer)
+ {
+ Type = type;
+ TypeSerializer = typeSerializer;
+ }
+
+ public DataType DataType => TypeSerializer.DataType;
+ public string CustomTypeName => TypeSerializer.TypeName;
}
}
+
+
}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/Types/CustomTypeSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/Types/CustomTypeSerializer.cs
new file mode 100644
index 0000000..d99fff4
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary/Types/CustomTypeSerializer.cs
@@ -0,0 +1,54 @@
+#region License
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#endregion
+
+using System.IO;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary.Types
+{
+ /// <summary>
+ /// The interface to implement for serializers of custom types.
+ /// </summary>
+ public abstract class CustomTypeSerializer : ITypeSerializer
+ {
+ /// <summary>
+ /// Gets the custom type name.
+ /// </summary>
+ public abstract string TypeName { get; }
+
+ /// <inheritdoc />
+ public DataType DataType => DataType.Custom;
+
+ /// <inheritdoc />
+ public abstract Task WriteAsync(object value, Stream stream, GraphBinaryWriter writer);
+
+ /// <inheritdoc />
+ public abstract Task WriteValueAsync(object value, Stream stream, GraphBinaryWriter writer, bool nullable);
+
+ /// <inheritdoc />
+ public abstract Task<object> ReadAsync(Stream stream, GraphBinaryReader reader);
+
+ /// <inheritdoc />
+ public abstract Task<object> ReadValueAsync(Stream stream, GraphBinaryReader reader, bool nullable);
+ }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary/Types/Sample/SamplePerson.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary/Types/Sample/SamplePerson.cs
new file mode 100644
index 0000000..e474f30
--- /dev/null
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary/Types/Sample/SamplePerson.cs
@@ -0,0 +1,59 @@
+#region License
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#endregion
+
+using System;
+
+namespace Gremlin.Net.UnitTest.Structure.IO.GraphBinary.Types.Sample
+{
+ public class SamplePerson : IEquatable<SamplePerson>
+ {
+ public SamplePerson(string name, DateTimeOffset birthDate)
+ {
+ Name = name;
+ BirthDate = birthDate;
+ }
+
+ public string Name { get; }
+ public DateTimeOffset BirthDate { get; }
+
+ public bool Equals(SamplePerson other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+ return Name == other.Name && BirthDate.Equals(other.BirthDate);
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != this.GetType()) return false;
+ return Equals((SamplePerson)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(Name, BirthDate);
+ }
+ }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary/Types/Sample/SamplePersonSerializer.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary/Types/Sample/SamplePersonSerializer.cs
new file mode 100644
index 0000000..b6566a5
--- /dev/null
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary/Types/Sample/SamplePersonSerializer.cs
@@ -0,0 +1,117 @@
+#region License
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#endregion
+
+using System;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Gremlin.Net.Structure.IO.GraphBinary;
+using Gremlin.Net.Structure.IO.GraphBinary.Types;
+
+namespace Gremlin.Net.UnitTest.Structure.IO.GraphBinary.Types.Sample
+{
+ public class SamplePersonSerializer : CustomTypeSerializer
+ {
+ private readonly byte[] _typeInfoBytes = { 0, 0, 0, 0 };
+
+ public override async Task WriteAsync(object value, Stream stream, GraphBinaryWriter writer)
+ {
+ // Write {custom type info}, {value_flag} and {value}
+ await stream.WriteAsync(_typeInfoBytes).ConfigureAwait(false);
+ await WriteValueAsync(value, stream, writer, true);
+ }
+
+ public override async Task WriteValueAsync(object value, Stream stream, GraphBinaryWriter writer, bool nullable)
+ {
+ if (value == null)
+ {
+ if (!nullable)
+ {
+ throw new IOException("Unexpected null value when nullable is false");
+ }
+
+ await writer.WriteValueFlagNullAsync(stream).ConfigureAwait(false);
+ return;
+ }
+
+ if (nullable)
+ {
+ await writer.WriteValueFlagNoneAsync(stream).ConfigureAwait(false);
+ }
+
+ var samplePerson = (SamplePerson)value;
+ var name = samplePerson.Name;
+
+ // value_length = name_byte_length + name_bytes + long
+ await stream.WriteIntAsync(4 + Encoding.UTF8.GetBytes(name).Length + 8).ConfigureAwait(false);
+
+ await writer.WriteValueAsync(name, stream, false).ConfigureAwait(false);
+ await writer.WriteValueAsync(samplePerson.BirthDate, stream, false).ConfigureAwait(false);
+ }
+
+ public override async Task<object> ReadAsync(Stream stream, GraphBinaryReader reader)
+ {
+ // {custom type info}, {value_flag} and {value}
+ // No custom_type_info
+ if (await stream.ReadIntAsync().ConfigureAwait(false) != 0)
+ {
+ throw new IOException("{custom_type_info} should not be provided for this custom type");
+ }
+
+ return await ReadValueAsync(stream, reader, true).ConfigureAwait(false);
+ }
+
+ public override async Task<object> ReadValueAsync(Stream stream, GraphBinaryReader reader, bool nullable)
+ {
+ if (nullable)
+ {
+ var valueFlag = await stream.ReadByteAsync().ConfigureAwait(false);
+ if ((valueFlag & 1) == 1)
+ {
+ return null;
+ }
+ }
+
+ // Read the byte length of the value bytes
+ var valueLength = await stream.ReadIntAsync().ConfigureAwait(false);
+
+ if (valueLength <= 0)
+ {
+ throw new IOException($"Unexpected value length: {valueLength}");
+ }
+
+ if (valueLength > stream.Length)
+ {
+ throw new IOException($"Not enough readable bytes: {valueLength} (expected: {stream.Length})");
+ }
+
+ var name = (string) await reader.ReadValueAsync<string>(stream, false).ConfigureAwait(false);
+ var birthDate =
+ (DateTimeOffset)await reader.ReadValueAsync<DateTimeOffset>(stream, false).ConfigureAwait(false);
+
+ return new SamplePerson(name, birthDate);
+ }
+
+ public override string TypeName => "sampleProvider.SamplePerson";
+ }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary/Types/Sample/SamplePersonSerializerTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary/Types/Sample/SamplePersonSerializerTests.cs
new file mode 100644
index 0000000..09ea734
--- /dev/null
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary/Types/Sample/SamplePersonSerializerTests.cs
@@ -0,0 +1,77 @@
+#region License
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#endregion
+
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Gremlin.Net.Structure.IO.GraphBinary;
+using Xunit;
+
+namespace Gremlin.Net.UnitTest.Structure.IO.GraphBinary.Types.Sample
+{
+ public class SamplePersonSerializerTests
+ {
+ [Fact]
+ public async Task TestCustomSerializationWithPerson()
+ {
+ var expected = new SamplePerson("Olivia", new DateTimeOffset(2010, 4, 29, 5, 30, 3, TimeSpan.FromHours(1)));
+ var registry = TypeSerializerRegistry.Build()
+ .AddCustomType(typeof(SamplePerson), new SamplePersonSerializer()).Create();
+ var writer = CreateGraphBinaryWriter(registry);
+ var reader = CreateGraphBinaryReader(registry);
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = (SamplePerson) await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public async Task ReadValueAndWriteValueShouldBeSymmetric(bool nullable)
+ {
+ var expected = new SamplePerson("Olivia", new DateTimeOffset(2010, 4, 29, 5, 30, 3, TimeSpan.FromHours(1)));
+ var registry = TypeSerializerRegistry.Build()
+ .AddCustomType(typeof(SamplePerson), new SamplePersonSerializer()).Create();
+ var writer = CreateGraphBinaryWriter(registry);
+ var reader = CreateGraphBinaryReader(registry);
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteValueAsync(expected, serializationStream, nullable).ConfigureAwait(false);
+ serializationStream.Position = 0;
+ var actual = (SamplePerson)await reader.ReadValueAsync<SamplePerson>(serializationStream, nullable)
+ .ConfigureAwait(false);
+
+ Assert.Equal(expected, actual);
+ }
+
+ private static GraphBinaryWriter CreateGraphBinaryWriter(TypeSerializerRegistry registry) =>
+ new GraphBinaryWriter(registry);
+
+ private static GraphBinaryReader CreateGraphBinaryReader(TypeSerializerRegistry registry) =>
+ new GraphBinaryReader(registry);
+ }
+}
\ No newline at end of file