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