From 2c2db9f42719fec178e12f18512eb31d57a31931 Mon Sep 17 00:00:00 2001
From: Ken Hu <106191785+kenhuuu@users.noreply.github.com>
Date: Mon, 2 Mar 2026 16:03:21 -0800
Subject: [PATCH 1/2] Add GraphBinary4 type serializers to gremlin-dotnet
This is just the type serializers but not the message serializers which
will be implemented later. Uses the same style as GraphBinary1 despite
not having custom types.
---
.../Structure/IO/GraphBinary4/DataType.cs | 142 ++++
.../IO/GraphBinary4/GraphBinaryReader.cs | 96 +++
.../IO/GraphBinary4/GraphBinaryWriter.cs | 139 +++
.../IO/GraphBinary4/ITypeSerializer.cs | 102 +++
.../Structure/IO/GraphBinary4/Marker.cs | 86 ++
.../IO/GraphBinary4/StreamExtensions.cs | 281 +++++++
.../IO/GraphBinary4/TypeSerializerRegistry.cs | 219 +++++
.../IO/GraphBinary4/Types/ArraySerializer.cs | 65 ++
.../Types/BigDecimalSerializer.cs | 95 +++
.../Types/BigIntegerSerializer.cs | 62 ++
.../IO/GraphBinary4/Types/BinarySerializer.cs | 61 ++
.../IO/GraphBinary4/Types/CharSerializer.cs | 87 ++
.../GraphBinary4/Types/DateTimeSerializer.cs | 78 ++
.../GraphBinary4/Types/DurationSerializer.cs | 63 ++
.../IO/GraphBinary4/Types/EdgeSerializer.cs | 89 ++
.../IO/GraphBinary4/Types/EnumSerializer.cs | 93 ++
.../IO/GraphBinary4/Types/ListSerializer.cs | 95 +++
.../IO/GraphBinary4/Types/MapSerializer.cs | 76 ++
.../IO/GraphBinary4/Types/PathSerializer.cs | 74 ++
.../GraphBinary4/Types/PropertySerializer.cs | 68 ++
.../IO/GraphBinary4/Types/SetSerializer.cs | 77 ++
.../Types/SimpleTypeSerializer.cs | 143 ++++
.../Types/SingleTypeSerializer.cs | 133 +++
.../IO/GraphBinary4/Types/StringSerializer.cs | 62 ++
.../IO/GraphBinary4/Types/UuidSerializer.cs | 111 +++
.../Types/VertexPropertySerializer.cs | 78 ++
.../IO/GraphBinary4/Types/VertexSerializer.cs | 68 ++
.../IO/GraphBinary4/GraphBinary4Tests.cs | 794 ++++++++++++++++++
.../Structure/IO/GraphBinary4/Model.cs | 210 +++++
.../IO/GraphBinary4/RoundTripTests.cs | 364 ++++++++
30 files changed, 4111 insertions(+)
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReader.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryWriter.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/ITypeSerializer.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Marker.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/StreamExtensions.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/TypeSerializerRegistry.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/ArraySerializer.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BigDecimalSerializer.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BigIntegerSerializer.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BinarySerializer.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/CharSerializer.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/DateTimeSerializer.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/DurationSerializer.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/EdgeSerializer.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/EnumSerializer.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/ListSerializer.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/MapSerializer.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PathSerializer.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PropertySerializer.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SetSerializer.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SimpleTypeSerializer.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SingleTypeSerializer.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/StringSerializer.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/UuidSerializer.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/VertexPropertySerializer.cs
create mode 100644 gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/VertexSerializer.cs
create mode 100644 gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/GraphBinary4Tests.cs
create mode 100644 gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/Model.cs
create mode 100644 gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/RoundTripTests.cs
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs
new file mode 100644
index 00000000000..8ae3199f163
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs
@@ -0,0 +1,142 @@
+#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.Structure.IO.GraphBinary4
+{
+ ///
+ /// Represents a GraphBinary 4.0 data type.
+ ///
+ public class DataType : IEquatable
+ {
+#pragma warning disable 1591
+ public static readonly DataType Int = new DataType(0x01);
+ public static readonly DataType Long = new DataType(0x02);
+ public static readonly DataType String = new DataType(0x03);
+ public static readonly DataType DateTime = new DataType(0x04);
+ public static readonly DataType Double = new DataType(0x07);
+ public static readonly DataType Float = new DataType(0x08);
+ public static readonly DataType List = new DataType(0x09);
+ public static readonly DataType Map = new DataType(0x0A);
+ public static readonly DataType Set = new DataType(0x0B);
+ public static readonly DataType Uuid = new DataType(0x0C);
+ public static readonly DataType Edge = new DataType(0x0D);
+ public static readonly DataType Path = new DataType(0x0E);
+ public static readonly DataType Property = new DataType(0x0F);
+ // Not yet implemented
+ // public static readonly DataType Graph = new DataType(0x10);
+ public static readonly DataType Vertex = new DataType(0x11);
+ public static readonly DataType VertexProperty = new DataType(0x12);
+ public static readonly DataType Direction = new DataType(0x18);
+ public static readonly DataType T = new DataType(0x20);
+ public static readonly DataType Merge = new DataType(0x21);
+ public static readonly DataType BigDecimal = new DataType(0x22);
+ public static readonly DataType BigInteger = new DataType(0x23);
+ public static readonly DataType Byte = new DataType(0x24);
+ public static readonly DataType Binary = new DataType(0x25);
+ public static readonly DataType Short = new DataType(0x26);
+ public static readonly DataType Boolean = new DataType(0x27);
+ // Not yet implemented
+ // public static readonly DataType Tree = new DataType(0x2B);
+ // public static readonly DataType CompositePDT = new DataType(0xF0);
+ // public static readonly DataType PrimitivePDT = new DataType(0xF1);
+ public static readonly DataType Char = new DataType(0x80);
+ public static readonly DataType Duration = new DataType(0x81);
+ public static readonly DataType Marker = new DataType(0xFD);
+#pragma warning restore 1591
+
+ ///
+ /// A null value for an unspecified Object value.
+ ///
+ public static readonly DataType UnspecifiedNull = new DataType(0xFE);
+
+ private DataType(int code)
+ {
+ TypeCode = (byte) code;
+ }
+
+ ///
+ /// Gets the type code of this data type.
+ ///
+ public byte TypeCode { get; }
+
+ ///
+ /// Creates a new instance for the given type code.
+ ///
+ public static DataType FromTypeCode(int code)
+ {
+ return new DataType(code);
+ }
+
+ ///
+ public bool Equals(DataType? other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+ return TypeCode == other.TypeCode;
+ }
+
+ ///
+ public override bool Equals(object? obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != GetType()) return false;
+ return Equals((DataType) obj);
+ }
+
+ ///
+ public override int GetHashCode()
+ {
+ return TypeCode.GetHashCode();
+ }
+
+ ///
+ /// Determines whether two specified have the same values.
+ ///
+ public static bool operator ==(DataType? first, DataType? second)
+ {
+ if (ReferenceEquals(null, first))
+ {
+ return ReferenceEquals(null, second);
+ }
+
+ return first.Equals(second);
+ }
+
+ ///
+ /// Determines whether two specified have different values.
+ ///
+ public static bool operator !=(DataType? first, DataType? second)
+ {
+ return !(first == second);
+ }
+
+ ///
+ public override string ToString()
+ {
+ return $"DataType{{ TypeCode = {TypeCode} }}";
+ }
+ }
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReader.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReader.cs
new file mode 100644
index 00000000000..ffe9f08f97a
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryReader.cs
@@ -0,0 +1,96 @@
+#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;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4
+{
+ ///
+ /// Allows to deserialize objects from GraphBinary v4.
+ ///
+ public class GraphBinaryReader
+ {
+ private readonly TypeSerializerRegistry _registry;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The to use for deserialization.
+ public GraphBinaryReader(TypeSerializerRegistry? registry = null)
+ {
+ _registry = registry ?? TypeSerializerRegistry.Instance;
+ }
+
+ ///
+ /// Reads only the value for a specific type .
+ ///
+ /// The GraphBinary data to parse.
+ /// The token to cancel the operation. The default value is None.
+ /// The type of the object to read.
+ /// The read value.
+ public async Task ReadNullableValueAsync(Stream stream,
+ CancellationToken cancellationToken = default)
+ {
+ var typedSerializer = _registry.GetSerializerFor(typeof(T));
+ return await typedSerializer.ReadNullableValueAsync(stream, this, cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ ///
+ /// Reads only the value for a specific type .
+ ///
+ /// The GraphBinary data to parse.
+ /// The token to cancel the operation. The default value is None.
+ /// The type of the object to read.
+ /// The read value.
+ public async Task ReadNonNullableValueAsync(Stream stream,
+ CancellationToken cancellationToken = default)
+ {
+ var typedSerializer = _registry.GetSerializerFor(typeof(T));
+ return await typedSerializer.ReadNonNullableValueAsync(stream, this, cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ ///
+ /// Reads the type code, information and value with fully-qualified format.
+ ///
+ /// The GraphBinary data to parse.
+ /// The token to cancel the operation. The default value is None.
+ /// The read value.
+ public async Task ReadAsync(Stream stream, CancellationToken cancellationToken = default)
+ {
+ var type = DataType.FromTypeCode(await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false));
+
+ if (type == DataType.UnspecifiedNull)
+ {
+ await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false); // read value byte to advance the index
+ return null;
+ }
+
+ var typeSerializer = _registry.GetSerializerFor(type);
+ return await typeSerializer.ReadAsync(stream, this, cancellationToken).ConfigureAwait(false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryWriter.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryWriter.cs
new file mode 100644
index 00000000000..c5c4fc6a7d9
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/GraphBinaryWriter.cs
@@ -0,0 +1,139 @@
+#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;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4
+{
+ ///
+ /// Allows to serialize objects to GraphBinary v4.
+ ///
+ public class GraphBinaryWriter
+ {
+ private const byte ValueFlagNull = 1;
+ private const byte ValueFlagNone = 0;
+
+ ///
+ /// A representing the version of the GraphBinary v4 specification.
+ ///
+ public const byte VersionByte = 0x84;
+
+ private static readonly byte[] UnspecifiedNullBytes = {DataType.UnspecifiedNull.TypeCode, 0x01};
+
+ private readonly TypeSerializerRegistry _registry;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The to use for serialization.
+ public GraphBinaryWriter(TypeSerializerRegistry? registry = null)
+ {
+ _registry = registry ?? TypeSerializerRegistry.Instance;
+ }
+
+ ///
+ /// Writes a nullable value without including type information.
+ ///
+ /// The value to write.
+ /// The stream to write to.
+ /// The token to cancel the operation. The default value is None.
+ /// A task that represents the asynchronous write operation.
+ public async Task WriteNullableValueAsync(object? value, Stream stream,
+ CancellationToken cancellationToken = default)
+ {
+ if (value == null)
+ {
+ await WriteValueFlagNullAsync(stream, cancellationToken).ConfigureAwait(false);
+ return;
+ }
+
+ var valueType = value.GetType();
+ var serializer = _registry.GetSerializerFor(valueType);
+ await serializer.WriteNullableValueAsync(value, stream, this, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Writes a non-nullable value without including type information.
+ ///
+ /// The value to write.
+ /// The stream to write to.
+ /// The token to cancel the operation. The default value is None.
+ /// A task that represents the asynchronous write operation.
+ public async Task WriteNonNullableValueAsync(object value, Stream stream,
+ CancellationToken cancellationToken = default)
+ {
+ if (value == null) throw new IOException($"{nameof(value)} cannot be null");
+ var valueType = value.GetType();
+ var serializer = _registry.GetSerializerFor(valueType);
+ await serializer.WriteNonNullableValueAsync(value, stream, this, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Writes an object in fully-qualified format, containing {type_code}{type_info}{value_flag}{value}.
+ ///
+ /// The value to write.
+ /// The stream to write to.
+ /// The token to cancel the operation. The default value is None.
+ /// A task that represents the asynchronous write operation.
+ public async Task WriteAsync(object? value, Stream stream, CancellationToken cancellationToken = default)
+ {
+ if (value == null)
+ {
+ await stream.WriteAsync(UnspecifiedNullBytes, cancellationToken).ConfigureAwait(false);
+ return;
+ }
+
+ var valueType = value.GetType();
+ var serializer = _registry.GetSerializerFor(valueType);
+
+ await stream.WriteByteAsync(serializer.DataType.TypeCode, cancellationToken).ConfigureAwait(false);
+ await serializer.WriteAsync(value, stream, this, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Writes a single byte representing the null value_flag.
+ ///
+ /// The stream to write to.
+ /// The token to cancel the operation. The default value is None.
+ /// A task that represents the asynchronous write operation.
+ public async Task WriteValueFlagNullAsync(Stream stream, CancellationToken cancellationToken = default)
+ {
+ await stream.WriteByteAsync(ValueFlagNull, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Writes a single byte with value 0, representing an unset value_flag.
+ ///
+ /// The stream to write to.
+ /// The token to cancel the operation. The default value is None.
+ /// A task that represents the asynchronous write operation.
+ public async Task WriteValueFlagNoneAsync(Stream stream, CancellationToken cancellationToken = default) {
+ await stream.WriteByteAsync(ValueFlagNone, cancellationToken).ConfigureAwait(false);
+ }
+
+
+ }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/ITypeSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/ITypeSerializer.cs
new file mode 100644
index 00000000000..9a195ef8126
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/ITypeSerializer.cs
@@ -0,0 +1,102 @@
+#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;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4
+{
+ ///
+ /// Represents a serializer for a certain type.
+ ///
+ public interface ITypeSerializer
+ {
+ ///
+ /// Gets the that supported by this serializer.
+ ///
+ DataType DataType { get; }
+
+ ///
+ /// Writes the type code, information and value to a stream.
+ ///
+ /// The value to write.
+ /// The stream to write to.
+ /// A that can be used to write nested values.
+ /// The token to cancel the operation. The default value is None.
+ /// A task that represents the asynchronous write operation.
+ Task WriteAsync(object value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Writes the nullable value to a stream, composed by the value flag and the sequence of bytes.
+ ///
+ /// The value to write.
+ /// The stream to write to.
+ /// A that can be used to write nested values.
+ /// The token to cancel the operation. The default value is None.
+ /// A task that represents the asynchronous write operation.
+ Task WriteNullableValueAsync(object? value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Writes the non-nullable value to a stream, composed by the value flag and the sequence of bytes.
+ ///
+ /// The value to write.
+ /// The stream to write to.
+ /// A that can be used to write nested values.
+ /// The token to cancel the operation. The default value is None.
+ /// A task that represents the asynchronous write operation.
+ Task WriteNonNullableValueAsync(object value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Reads the type information and value from the stream.
+ ///
+ /// The GraphBinary data to parse.
+ /// A that can be used to read nested values.
+ /// The token to cancel the operation. The default value is None.
+ /// The read value.
+ Task ReadAsync(Stream stream, GraphBinaryReader reader, CancellationToken cancellationToken = default);
+
+ ///
+ /// Reads the value from the stream (not the type information).
+ ///
+ /// The GraphBinary data to parse.
+ /// A that can be used to read nested values.
+ /// The token to cancel the operation. The default value is None.
+ /// The read value.
+ Task ReadNullableValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// Reads the value from the stream (not the type information).
+ ///
+ /// The GraphBinary data to parse.
+ /// A that can be used to read nested values.
+ /// The token to cancel the operation. The default value is None.
+ /// The read value.
+ Task ReadNonNullableValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default);
+ }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Marker.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Marker.cs
new file mode 100644
index 00000000000..d13f1d3fbb0
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Marker.cs
@@ -0,0 +1,86 @@
+#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.Structure.IO.GraphBinary4
+{
+ ///
+ /// Represents a special marker type for stream boundaries in GraphBinary 4.0 serialization.
+ ///
+ ///
+ /// Markers are used to indicate stream boundaries in GraphBinary 4.0 responses.
+ /// The marker indicates the end of result data in a response,
+ /// signaling that the footer (status code, message, exception) follows.
+ ///
+ public sealed class Marker
+ {
+ ///
+ /// Marker indicating the end of the result stream.
+ ///
+ ///
+ /// In GraphBinary 4.0 response format, this marker (type code 0xFD with value 0x00)
+ /// separates the result data from the response footer.
+ ///
+ public static readonly Marker EndOfStream = new Marker(0);
+
+ ///
+ /// Gets the byte value of this marker as used on the wire.
+ ///
+ public byte Value { get; }
+
+ private Marker(byte value)
+ {
+ Value = value;
+ }
+
+ ///
+ /// Returns the instance for the given byte value.
+ ///
+ /// The marker byte value.
+ /// The corresponding instance.
+ /// Thrown if the value is not a known marker.
+ public static Marker Of(byte value)
+ {
+ if (value != 0)
+ {
+ throw new ArgumentException($"Unknown marker value: {value}", nameof(value));
+ }
+ return EndOfStream;
+ }
+
+ ///
+ public override string ToString() => $"Marker({Value})";
+
+ ///
+ public override bool Equals(object? obj)
+ {
+ if (obj is null) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ return obj is Marker other && Value == other.Value;
+ }
+
+ ///
+ public override int GetHashCode() => Value.GetHashCode();
+ }
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/StreamExtensions.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/StreamExtensions.cs
new file mode 100644
index 00000000000..e85767289f0
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/StreamExtensions.cs
@@ -0,0 +1,281 @@
+#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;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4
+{
+ ///
+ /// Provides extension methods for that are mostly useful when implementing GraphBinary
+ /// serializers.
+ ///
+ public static class StreamExtensions
+ {
+ ///
+ /// Asynchronously writes a to a .
+ ///
+ /// The to write the to.
+ /// The to write.
+ /// The token to cancel the operation. The default value is None.
+ public static async Task WriteByteAsync(this Stream stream, byte value,
+ CancellationToken cancellationToken = default)
+ {
+ await stream.WriteAsync(new[] {value}, 0, 1, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Asynchronously reads a from a .
+ ///
+ /// The to read from.
+ /// The token to cancel the operation. The default value is None.
+ /// The read .
+ public static async Task ReadByteAsync(this Stream stream, CancellationToken cancellationToken = default)
+ {
+ var readBuffer = new byte[1];
+ await stream.ReadAsync(readBuffer, 0, 1, cancellationToken).ConfigureAwait(false);
+ return readBuffer[0];
+ }
+
+ ///
+ /// Asynchronously writes a to a .
+ ///
+ /// The to write the to.
+ /// The to write.
+ /// The token to cancel the operation. The default value is None.
+ public static async Task WriteSByteAsync(this Stream stream, sbyte value,
+ CancellationToken cancellationToken = default)
+ {
+ await stream.WriteByteAsync((byte)value, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Asynchronously reads a from a .
+ ///
+ /// The to read from.
+ /// The token to cancel the operation. The default value is None.
+ /// The read .
+ public static async Task ReadSByteAsync(this Stream stream, CancellationToken cancellationToken = default)
+ {
+ return (sbyte)await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Asynchronously writes an to a .
+ ///
+ /// The to write the to.
+ /// The to write.
+ /// The token to cancel the operation. The default value is None.
+ public static async Task WriteIntAsync(this Stream stream, int value,
+ CancellationToken cancellationToken = default)
+ {
+ var bytes = BitConverter.GetBytes(value);
+ await stream.WriteAsync(new[] { bytes[3], bytes[2], bytes[1], bytes[0] }, 0, 4, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Asynchronously reads an from a .
+ ///
+ /// The to read from.
+ /// The token to cancel the operation. The default value is None.
+ /// The read .
+ public static async Task ReadIntAsync(this Stream stream, CancellationToken cancellationToken = default)
+ {
+ var bytes = new byte[4];
+ await stream.ReadAsync(bytes, 0, 4, cancellationToken).ConfigureAwait(false);
+ return BitConverter.ToInt32(new []{bytes[3], bytes[2], bytes[1], bytes[0]}, 0);
+ }
+
+ ///
+ /// Asynchronously writes a to a .
+ ///
+ /// The to write the to.
+ /// The to write.
+ /// The token to cancel the operation. The default value is None.
+ public static async Task WriteLongAsync(this Stream stream, long value,
+ CancellationToken cancellationToken = default)
+ {
+ var bytes = BitConverter.GetBytes(value);
+ await stream
+ .WriteAsync(new[] { bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0] }, 0,
+ 8, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Asynchronously reads a from a .
+ ///
+ /// The to read from.
+ /// The token to cancel the operation. The default value is None.
+ /// The read .
+ public static async Task ReadLongAsync(this Stream stream, CancellationToken cancellationToken = default)
+ {
+ var bytes = new byte[8];
+ await stream.ReadAsync(bytes, 0, 8, cancellationToken).ConfigureAwait(false);
+ return BitConverter.ToInt64(
+ new[] {bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0]}, 0);
+ }
+
+ ///
+ /// Asynchronously writes a to a .
+ ///
+ /// The to write the to.
+ /// The to write.
+ /// The token to cancel the operation. The default value is None.
+ public static async Task WriteFloatAsync(this Stream stream, float value,
+ CancellationToken cancellationToken = default)
+ {
+ var bytes = BitConverter.GetBytes(value);
+ await stream.WriteAsync(new[] { bytes[3], bytes[2], bytes[1], bytes[0] }, 0, 4, cancellationToken)
+ .ConfigureAwait(false);
+ }
+
+ ///
+ /// Asynchronously reads a from a .
+ ///
+ /// The to read from.
+ /// The token to cancel the operation. The default value is None.
+ /// The read .
+ public static async Task ReadFloatAsync(this Stream stream,
+ CancellationToken cancellationToken = default)
+ {
+ var bytes = new byte[4];
+ await stream.ReadAsync(bytes, 0, 4, cancellationToken).ConfigureAwait(false);
+ return BitConverter.ToSingle(new []{bytes[3], bytes[2], bytes[1], bytes[0]}, 0);
+ }
+
+ ///
+ /// Asynchronously writes a to a .
+ ///
+ /// The to write the to.
+ /// The to write.
+ /// The token to cancel the operation. The default value is None.
+ public static async Task WriteDoubleAsync(this Stream stream, double value,
+ CancellationToken cancellationToken = default)
+ {
+ var bytes = BitConverter.GetBytes(value);
+ await stream
+ .WriteAsync(new[] {bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0]}, 0,
+ 8, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Asynchronously reads a from a .
+ ///
+ /// The to read from.
+ /// The token to cancel the operation. The default value is None.
+ /// The read .
+ public static async Task ReadDoubleAsync(this Stream stream,
+ CancellationToken cancellationToken = default)
+ {
+ var bytes = new byte[8];
+ await stream.ReadAsync(bytes, 0, 8, cancellationToken).ConfigureAwait(false);
+ return BitConverter.ToDouble(
+ new[] {bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2], bytes[1], bytes[0]}, 0);
+ }
+
+ ///
+ /// Asynchronously writes a to a .
+ ///
+ /// The to write the to.
+ /// The to write.
+ /// The token to cancel the operation. The default value is None.
+ public static async Task WriteShortAsync(this Stream stream, short value,
+ CancellationToken cancellationToken = default)
+ {
+ var bytes = BitConverter.GetBytes(value);
+ await stream.WriteAsync(new[] { bytes[1], bytes[0] }, 0, 2, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Asynchronously reads a from a .
+ ///
+ /// The to read from.
+ /// The token to cancel the operation. The default value is None.
+ /// The read .
+ public static async Task ReadShortAsync(this Stream stream,
+ CancellationToken cancellationToken = default)
+ {
+ var bytes = new byte[2];
+ await stream.ReadAsync(bytes, 0, 2, cancellationToken).ConfigureAwait(false);
+ return BitConverter.ToInt16(new []{bytes[1], bytes[0]}, 0);
+ }
+
+ ///
+ /// Asynchronously writes a to a .
+ ///
+ /// The to write the to.
+ /// The to write.
+ /// The token to cancel the operation. The default value is None.
+ public static async Task WriteBoolAsync(this Stream stream, bool value,
+ CancellationToken cancellationToken = default)
+ {
+ await stream.WriteByteAsync((byte)(value ? 1 : 0), cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Asynchronously reads a from a .
+ ///
+ /// The to read from.
+ /// The token to cancel the operation. The default value is None.
+ /// The read .
+ public static async Task ReadBoolAsync(this Stream stream, CancellationToken cancellationToken = default)
+ {
+ var b = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+ return b switch
+ {
+ 1 => true,
+ 0 => false,
+ _ => throw new IOException($"Cannot read byte {b} as a boolean.")
+ };
+ }
+
+ ///
+ /// Asynchronously writes a to a .
+ ///
+ /// The to write the to.
+ /// The to write.
+ /// The token to cancel the operation. The default value is None.
+ public static async Task WriteAsync(this Stream stream, byte[] value,
+ CancellationToken cancellationToken = default)
+ {
+ await stream.WriteAsync(value, 0, value.Length, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Asynchronously reads a from a into a buffer.
+ ///
+ /// The to read from.
+ /// The number of bytes to read.
+ /// The token to cancel the operation. The default value is None.
+ /// The read .
+ public static async Task ReadAsync(this Stream stream, int count,
+ CancellationToken cancellationToken = default)
+ {
+ var buffer = new byte[count];
+ await stream.ReadAsync(buffer, 0, count, cancellationToken).ConfigureAwait(false);
+ return buffer;
+ }
+ }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/TypeSerializerRegistry.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/TypeSerializerRegistry.cs
new file mode 100644
index 00000000000..82421f14a13
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/TypeSerializerRegistry.cs
@@ -0,0 +1,219 @@
+#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.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.IO;
+using System.Linq;
+using System.Numerics;
+using Gremlin.Net.Process.Traversal;
+using Gremlin.Net.Structure.IO.GraphBinary4.Types;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4
+{
+ ///
+ /// Provides GraphBinary 4.0 serializers for different types.
+ ///
+ public class TypeSerializerRegistry
+ {
+ private readonly Dictionary _serializerByType =
+ new Dictionary
+ {
+ {typeof(int), SingleTypeSerializers.IntSerializer},
+ {typeof(long), SingleTypeSerializers.LongSerializer},
+ {typeof(string), new StringSerializer()},
+ {typeof(DateTimeOffset), new DateTimeSerializer()},
+ {typeof(double), SingleTypeSerializers.DoubleSerializer},
+ {typeof(float), SingleTypeSerializers.FloatSerializer},
+ {typeof(Guid), new UuidSerializer()},
+ {typeof(Edge), new EdgeSerializer()},
+ {typeof(Path), new PathSerializer()},
+ {typeof(Property), new PropertySerializer()},
+ {typeof(Vertex), new VertexSerializer()},
+ {typeof(VertexProperty), new VertexPropertySerializer()},
+ {typeof(Direction), EnumSerializers.DirectionSerializer},
+ {typeof(Merge), EnumSerializers.MergeSerializer},
+ {typeof(T), EnumSerializers.TSerializer},
+ {typeof(decimal), new BigDecimalSerializer()},
+ {typeof(BigInteger), new BigIntegerSerializer()},
+ {typeof(sbyte), SingleTypeSerializers.ByteSerializer},
+ {typeof(byte[]), new BinarySerializer()},
+ {typeof(short), SingleTypeSerializers.ShortSerializer},
+ {typeof(bool), SingleTypeSerializers.BooleanSerializer},
+ {typeof(char), new CharSerializer()},
+ {typeof(TimeSpan), new DurationSerializer()},
+ {typeof(Marker), SingleTypeSerializers.MarkerSerializer},
+ };
+
+ private readonly Dictionary _serializerByDataType =
+ new Dictionary
+ {
+ {DataType.Int, SingleTypeSerializers.IntSerializer},
+ {DataType.Long, SingleTypeSerializers.LongSerializer},
+ {DataType.String, new StringSerializer()},
+ {DataType.DateTime, new DateTimeSerializer()},
+ {DataType.Double, SingleTypeSerializers.DoubleSerializer},
+ {DataType.Float, SingleTypeSerializers.FloatSerializer},
+ {DataType.List, new ListSerializer()},
+ {DataType.Map, new MapSerializer()},
+ {DataType.Set, new SetSerializer, object>()},
+ {DataType.Uuid, new UuidSerializer()},
+ {DataType.Edge, new EdgeSerializer()},
+ {DataType.Path, new PathSerializer()},
+ {DataType.Property, new PropertySerializer()},
+ {DataType.Vertex, new VertexSerializer()},
+ {DataType.VertexProperty, new VertexPropertySerializer()},
+ {DataType.Direction, EnumSerializers.DirectionSerializer},
+ {DataType.Merge, EnumSerializers.MergeSerializer},
+ {DataType.T, EnumSerializers.TSerializer},
+ {DataType.BigDecimal, new BigDecimalSerializer()},
+ {DataType.BigInteger, new BigIntegerSerializer()},
+ {DataType.Byte, SingleTypeSerializers.ByteSerializer},
+ {DataType.Binary, new BinarySerializer()},
+ {DataType.Short, SingleTypeSerializers.ShortSerializer},
+ {DataType.Boolean, SingleTypeSerializers.BooleanSerializer},
+ {DataType.Char, new CharSerializer()},
+ {DataType.Duration, new DurationSerializer()},
+ {DataType.Marker, SingleTypeSerializers.MarkerSerializer},
+ };
+
+ ///
+ /// Provides a default instance.
+ ///
+ public static readonly TypeSerializerRegistry Instance = new TypeSerializerRegistry();
+
+ ///
+ /// Gets a serializer for the given type of the value to be serialized.
+ ///
+ /// Type of the value to be serialized.
+ /// A serializer for the provided type.
+ /// Thrown when no serializer can be found for the type.
+ public ITypeSerializer GetSerializerFor(Type valueType)
+ {
+ if (_serializerByType.TryGetValue(valueType, out var serializerForType))
+ {
+ return serializerForType;
+ }
+
+ if (IsDictionaryType(valueType, out var dictKeyType, out var dictValueType))
+ {
+ var serializerType = typeof(MapSerializer<,>).MakeGenericType(dictKeyType, dictValueType);
+ var serializer = (ITypeSerializer?) Activator.CreateInstance(serializerType);
+ _serializerByType[valueType] = serializer ??
+ throw new IOException(
+ $"Cannot create a serializer for the dictionary type {valueType}.");
+ return serializer;
+ }
+
+ if (IsSetType(valueType))
+ {
+ var memberType = valueType.GetGenericArguments()[0];
+ var serializerType = typeof(SetSerializer<,>).MakeGenericType(valueType, memberType);
+ var serializer = (ITypeSerializer?) Activator.CreateInstance(serializerType);
+ _serializerByType[valueType] = serializer ??
+ throw new IOException(
+ $"Cannot create a serializer for the set type {valueType}.");
+ return serializer;
+ }
+
+ if (valueType.IsArray)
+ {
+ var memberType = valueType.GetElementType();
+ var serializerType = typeof(ArraySerializer<>).MakeGenericType(memberType!);
+ var serializer = (ITypeSerializer?) Activator.CreateInstance(serializerType);
+ _serializerByType[valueType] = serializer ??
+ throw new IOException(
+ $"Cannot create a serializer for the array type {valueType}.");
+ return serializer;
+ }
+
+ if (IsListType(valueType))
+ {
+ var memberType = valueType.GetGenericArguments()[0];
+ var serializerType = typeof(ListSerializer<>).MakeGenericType(memberType);
+ var serializer = (ITypeSerializer?) Activator.CreateInstance(serializerType);
+ _serializerByType[valueType] = serializer ??
+ throw new IOException(
+ $"Cannot create a serializer for the list type {valueType}.");
+ return serializer;
+ }
+
+ foreach (var supportedType in new List(_serializerByType.Keys))
+ {
+ if (supportedType.IsAssignableFrom(valueType))
+ {
+ var serializer = _serializerByType[supportedType];
+ _serializerByType[valueType] = serializer;
+ return serializer;
+ }
+ }
+
+ throw new InvalidOperationException($"No serializer found for type '{valueType}'.");
+ }
+
+ ///
+ /// Gets a serializer for the given GraphBinary 4.0 data type code.
+ ///
+ /// The GraphBinary 4.0 data type.
+ /// A serializer for the provided data type.
+ public ITypeSerializer GetSerializerFor(DataType dataType)
+ {
+ return _serializerByDataType[dataType];
+ }
+
+ private static bool IsDictionaryType(Type type, [NotNullWhen(returnValue: true)] out Type? keyType,
+ [NotNullWhen(returnValue: true)] out Type? valueType)
+ {
+ var maybeInterfaceType = type
+ .GetInterfaces()
+ .FirstOrDefault(implementedInterfaceType => implementedInterfaceType.IsConstructedGenericType && implementedInterfaceType.GetGenericTypeDefinition() == typeof(IDictionary<,>));
+
+ if (maybeInterfaceType is { } interfaceType)
+ {
+ keyType = interfaceType.GetGenericArguments()[0];
+ valueType = interfaceType.GetGenericArguments()[1];
+ return true;
+ }
+
+ keyType = null;
+ valueType = null;
+ return false;
+ }
+
+ private static bool IsSetType(Type type)
+ {
+ return type.GetInterfaces().Any(implementedInterface => implementedInterface.IsConstructedGenericType &&
+ implementedInterface.GetGenericTypeDefinition() ==
+ typeof(ISet<>));
+ }
+
+ private static bool IsListType(Type type)
+ {
+ return type.GetInterfaces().Any(implementedInterface => implementedInterface.IsConstructedGenericType &&
+ implementedInterface.GetGenericTypeDefinition() ==
+ typeof(IList<>));
+ }
+
+ }
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/ArraySerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/ArraySerializer.cs
new file mode 100644
index 00000000000..c4f939b5162
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/ArraySerializer.cs
@@ -0,0 +1,65 @@
+#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;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ ///
+ /// A serializer that serializes .NET arrays to GraphBinary4 lists.
+ ///
+ /// The type of elements in the array.
+ public class ArraySerializer : SimpleTypeSerializer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ArraySerializer() : base(DataType.List)
+ {
+ }
+
+ ///
+ protected override async Task WriteValueAsync(TMember[] value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ await writer.WriteNonNullableValueAsync(value.Length, stream, cancellationToken).ConfigureAwait(false);
+
+ foreach (var item in value)
+ {
+ await writer.WriteAsync(item, stream, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ /// Currently not supported as GraphBinary has no array data type.
+ ///
+ protected override Task ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ throw new NotImplementedException("Reading an array is not supported");
+ }
+ }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BigDecimalSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BigDecimalSerializer.cs
new file mode 100644
index 00000000000..e5bf0c79058
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BigDecimalSerializer.cs
@@ -0,0 +1,95 @@
+#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.Numerics;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ ///
+ /// A serializer that serializes values as BigDecimal in GraphBinary4.
+ ///
+ public class BigDecimalSerializer : SimpleTypeSerializer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public BigDecimalSerializer() : base(DataType.BigDecimal)
+ {
+ }
+
+ ///
+ protected override async Task WriteValueAsync(decimal value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ var (unscaledValue, scale) = GetUnscaledValueAndScale(value);
+ await writer.WriteNonNullableValueAsync(scale, stream, cancellationToken).ConfigureAwait(false);
+ await writer.WriteNonNullableValueAsync(unscaledValue, stream, cancellationToken).ConfigureAwait(false);
+ }
+
+ private static (BigInteger, int) GetUnscaledValueAndScale(decimal input)
+ {
+ var parts = decimal.GetBits(input);
+
+ var sign = (parts[3] & 0x80000000) != 0;
+
+ var scale = (parts[3] >> 16) & 0x7F;
+
+ var lowBytes = BitConverter.GetBytes(parts[0]);
+ var middleBytes = BitConverter.GetBytes(parts[1]);
+ var highBytes = BitConverter.GetBytes(parts[2]);
+ var valueBytes = new byte[12];
+ lowBytes.CopyTo(valueBytes, 0);
+ middleBytes.CopyTo(valueBytes, 4);
+ highBytes.CopyTo(valueBytes, 8);
+ var bigInt = new BigInteger(valueBytes);
+
+ if (sign)
+ {
+ bigInt = -bigInt;
+ }
+
+ return (bigInt, scale);
+ }
+
+ ///
+ protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ var scale = (int)await reader.ReadNonNullableValueAsync(stream, cancellationToken)
+ .ConfigureAwait(false);
+ var unscaled = (BigInteger)await reader.ReadNonNullableValueAsync(stream, cancellationToken)
+ .ConfigureAwait(false);
+
+ return ConvertScaleAndUnscaledValue(scale, unscaled);
+ }
+
+ private static decimal ConvertScaleAndUnscaledValue(int scale, BigInteger unscaledValue)
+ {
+ return (decimal) unscaledValue * (decimal) Math.Pow(10, -scale);
+ }
+ }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BigIntegerSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BigIntegerSerializer.cs
new file mode 100644
index 00000000000..4a555a0fad9
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BigIntegerSerializer.cs
@@ -0,0 +1,62 @@
+#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.Linq;
+using System.Numerics;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ ///
+ /// A serializer for values.
+ ///
+ public class BigIntegerSerializer : SimpleTypeSerializer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public BigIntegerSerializer() : base(DataType.BigInteger)
+ {
+ }
+
+ ///
+ protected override async Task WriteValueAsync(BigInteger value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ var bytes = value.ToByteArray().Reverse().ToArray();
+ await writer.WriteNonNullableValueAsync(bytes.Length, stream, cancellationToken).ConfigureAwait(false);
+ await stream.WriteAsync(bytes, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ var length = (int)await reader.ReadNonNullableValueAsync(stream, cancellationToken).ConfigureAwait(false);
+ var bytes = await stream.ReadAsync(length, cancellationToken).ConfigureAwait(false);
+ return new BigInteger(bytes.Reverse().ToArray());
+ }
+ }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BinarySerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BinarySerializer.cs
new file mode 100644
index 00000000000..58723e77a2e
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/BinarySerializer.cs
@@ -0,0 +1,61 @@
+#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;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+
+ ///
+ /// A serializer for byte[].
+ ///
+ public class BinarySerializer : SimpleTypeSerializer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public BinarySerializer() : base(DataType.Binary)
+ {
+ }
+
+ ///
+ protected override async Task WriteValueAsync(byte[] value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ await writer.WriteNonNullableValueAsync(value.Length, stream, cancellationToken).ConfigureAwait(false);
+ await stream.WriteAsync(value, 0, value.Length, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ var length = (int)await reader.ReadNonNullableValueAsync(stream, cancellationToken).ConfigureAwait(false);
+ var buffer = new byte[length];
+ await stream.ReadAsync(buffer, 0, length, cancellationToken).ConfigureAwait(false);
+ return buffer;
+ }
+ }
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/CharSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/CharSerializer.cs
new file mode 100644
index 00000000000..7ea3cb5cc5d
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/CharSerializer.cs
@@ -0,0 +1,87 @@
+#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.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ ///
+ /// A serializer.
+ ///
+ public class CharSerializer : SimpleTypeSerializer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public CharSerializer() : base(DataType.Char)
+ {
+ }
+
+ ///
+ protected override async Task WriteValueAsync(char value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ var bytes = Encoding.UTF8.GetBytes(value.ToString());
+ await stream.WriteAsync(bytes, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ var firstByte = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+ var byteLength = 1;
+ // A byte with the first byte ON (10000000) signals that more bytes are needed to represent the UTF-8 char
+ if ((firstByte & 0x80) > 0)
+ {
+ if ((firstByte & 0xf0) == 0xf0)
+ { // 0xf0 = 11110000
+ byteLength = 4;
+ } else if ((firstByte & 0xe0) == 0xe0)
+ { //11100000
+ byteLength = 3;
+ } else if ((firstByte & 0xc0) == 0xc0)
+ { //11000000
+ byteLength = 2;
+ }
+ }
+
+ byte[] bytes;
+ if (byteLength == 1)
+ {
+ bytes = new[] {firstByte};
+ }
+ else
+ {
+ bytes = new byte[byteLength];
+ bytes[0] = firstByte;
+ await stream.ReadAsync(bytes, 1, byteLength - 1, cancellationToken).ConfigureAwait(false);
+ }
+
+ return Encoding.UTF8.GetChars(bytes)[0];
+ }
+ }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/DateTimeSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/DateTimeSerializer.cs
new file mode 100644
index 00000000000..b5fc932af63
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/DateTimeSerializer.cs
@@ -0,0 +1,78 @@
+#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;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ ///
+ /// A serializer for values in GraphBinary 4.0.
+ ///
+ public class DateTimeSerializer : SimpleTypeSerializer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DateTimeSerializer() : base(DataType.DateTime)
+ {
+ }
+
+ ///
+ protected override async Task WriteValueAsync(DateTimeOffset value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ await stream.WriteIntAsync(value.Year, cancellationToken).ConfigureAwait(false);
+ await stream.WriteByteAsync((byte)value.Month, cancellationToken).ConfigureAwait(false);
+ await stream.WriteByteAsync((byte)value.Day, cancellationToken).ConfigureAwait(false);
+
+ // time as Long nanoseconds since midnight
+ var timeNanos = (value.Hour * 3600L + value.Minute * 60L + value.Second) * 1_000_000_000L +
+ value.Millisecond * 1_000_000L +
+ (value.Ticks % TimeSpan.TicksPerMillisecond) * 100L;
+ await stream.WriteLongAsync(timeNanos, cancellationToken).ConfigureAwait(false);
+
+ await stream.WriteIntAsync((int)value.Offset.TotalSeconds, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ var year = await stream.ReadIntAsync(cancellationToken).ConfigureAwait(false);
+ var month = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+ var day = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+ var timeNanos = await stream.ReadLongAsync(cancellationToken).ConfigureAwait(false);
+ var offsetSeconds = await stream.ReadIntAsync(cancellationToken).ConfigureAwait(false);
+
+ // Convert nanoseconds to TimeSpan (100 nanoseconds per tick)
+ var timeSpan = TimeSpan.FromTicks(timeNanos / 100);
+ var offset = TimeSpan.FromSeconds(offsetSeconds);
+
+ return new DateTimeOffset(year, month, day, timeSpan.Hours, timeSpan.Minutes, timeSpan.Seconds,
+ timeSpan.Milliseconds, offset).AddTicks(timeSpan.Ticks % TimeSpan.TicksPerMillisecond);
+ }
+ }
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/DurationSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/DurationSerializer.cs
new file mode 100644
index 00000000000..0b6e8d59a0e
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/DurationSerializer.cs
@@ -0,0 +1,63 @@
+#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;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ ///
+ /// A serializer that serializes values as Duration in GraphBinary.
+ ///
+ public class DurationSerializer : SimpleTypeSerializer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DurationSerializer() : base(DataType.Duration)
+ {
+ }
+
+ ///
+ protected override async Task WriteValueAsync(TimeSpan value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ await stream.WriteLongAsync((long)value.TotalSeconds, cancellationToken).ConfigureAwait(false);
+ // Get sub-second ticks and convert to nanoseconds (1 tick = 100 nanoseconds)
+ await stream.WriteIntAsync((int)(value.Ticks % TimeSpan.TicksPerSecond * 100), cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ var seconds = await stream.ReadLongAsync(cancellationToken).ConfigureAwait(false);
+ var nanoseconds = await stream.ReadIntAsync(cancellationToken).ConfigureAwait(false);
+
+ // Convert nanoseconds to ticks (1 tick = 100 nanoseconds)
+ return TimeSpan.FromSeconds(seconds) + TimeSpan.FromTicks(nanoseconds / 100);
+ }
+ }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/EdgeSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/EdgeSerializer.cs
new file mode 100644
index 00000000000..b3a8097de5e
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/EdgeSerializer.cs
@@ -0,0 +1,89 @@
+#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.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ ///
+ /// An serializer for GraphBinary 4.0.
+ /// In v4, labels are serialized as List of Strings instead of a single String.
+ ///
+ public class EdgeSerializer : SimpleTypeSerializer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public EdgeSerializer() : base(DataType.Edge)
+ {
+ }
+
+ ///
+ protected override async Task WriteValueAsync(Edge value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ await writer.WriteAsync(value.Id, stream, cancellationToken).ConfigureAwait(false);
+ await writer.WriteNonNullableValueAsync(new List { value.Label }, stream, cancellationToken).ConfigureAwait(false);
+
+ await writer.WriteAsync(value.InV.Id, stream, cancellationToken).ConfigureAwait(false);
+ await writer.WriteNonNullableValueAsync(new List { value.InV.Label }, stream, cancellationToken).ConfigureAwait(false);
+ await writer.WriteAsync(value.OutV.Id, stream, cancellationToken).ConfigureAwait(false);
+ await writer.WriteNonNullableValueAsync(new List { value.OutV.Label }, stream, cancellationToken).ConfigureAwait(false);
+
+ // Placeholder for the parent vertex
+ await writer.WriteAsync(null, stream, cancellationToken).ConfigureAwait(false);
+
+ // placeholder for the properties
+ await writer.WriteAsync(null, stream, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ var id = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
+ var labelList = (List)await reader.ReadNonNullableValueAsync>(stream, cancellationToken).ConfigureAwait(false);
+ var label = labelList.Count > 0 ? labelList[0] ?? "" : "";
+
+ var inVId = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
+ var inVLabelList = (List)await reader.ReadNonNullableValueAsync>(stream, cancellationToken).ConfigureAwait(false);
+ var inV = new Vertex(inVId, inVLabelList.Count > 0 ? inVLabelList[0] ?? "" : "");
+
+ var outVId = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
+ var outVLabelList = (List)await reader.ReadNonNullableValueAsync>(stream, cancellationToken).ConfigureAwait(false);
+ var outV = new Vertex(outVId, outVLabelList.Count > 0 ? outVLabelList[0] ?? "" : "");
+
+ // discard possible parent vertex
+ await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
+
+ var properties = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
+ var propertiesAsArray = null == properties ? Array.Empty() : (properties as List)?.ToArray();
+
+ return new Edge(id, outV, label, inV, propertiesAsArray);
+ }
+ }
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/EnumSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/EnumSerializer.cs
new file mode 100644
index 00000000000..c885c97bf9f
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/EnumSerializer.cs
@@ -0,0 +1,93 @@
+#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;
+using System.Threading.Tasks;
+using Gremlin.Net.Process.Traversal;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ ///
+ /// Provides serializers for enum types supported in GraphBinary 4.0.
+ ///
+ ///
+ /// GraphBinary 4.0 only supports Direction, T, and Merge enum types.
+ /// The following types were removed in v4: Barrier, Cardinality, Column, DT, GType,
+ /// Operator, Order, Pick, Pop, Scope.
+ ///
+ public static class EnumSerializers
+ {
+ ///
+ /// A serializer for values.
+ ///
+ public static readonly EnumSerializer DirectionSerializer =
+ new EnumSerializer(DataType.Direction, Direction.GetByValue);
+
+ ///
+ /// A serializer for values.
+ ///
+ public static readonly EnumSerializer MergeSerializer =
+ new EnumSerializer(DataType.Merge, Merge.GetByValue);
+
+ ///
+ /// A serializer for values.
+ ///
+ public static readonly EnumSerializer TSerializer =
+ new EnumSerializer(DataType.T, T.GetByValue);
+ }
+
+ ///
+ /// Generalized serializer for enum types.
+ ///
+ /// The type of the enum to serialize.
+ public class EnumSerializer : SimpleTypeSerializer
+ where TEnum : EnumWrapper
+ {
+ private readonly Func _readFunc;
+
+ internal EnumSerializer(DataType dataType, Func readFunc) : base(dataType)
+ {
+ _readFunc = readFunc;
+ }
+
+ ///
+ protected override async Task WriteValueAsync(TEnum value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ await writer.WriteAsync(value.EnumValue, stream, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ // This should probably be `reader.ReadNonNullableValueAsync(stream, cancellationToken)` instead,
+ // but it's the same in other GLVs and changing this would be a breaking change for the GraphBinary format.
+ var enumValue = (string?) await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
+ if (enumValue == null) throw new IOException($"Read null as a value for {DataType}");
+ return _readFunc.Invoke(enumValue);
+ }
+ }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/ListSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/ListSerializer.cs
new file mode 100644
index 00000000000..73c4179fac0
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/ListSerializer.cs
@@ -0,0 +1,95 @@
+#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.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ ///
+ /// A generic list serializer for GraphBinary 4.0.
+ ///
+ /// The type of elements in the list.
+ public class ListSerializer : SimpleTypeSerializer>
+ {
+ private const byte ValueFlagBulked = 0x02;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ListSerializer() : base(DataType.List)
+ {
+ }
+
+ ///
+ protected override async Task WriteValueAsync(IList value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ await writer.WriteNonNullableValueAsync(value.Count, stream, cancellationToken).ConfigureAwait(false);
+
+ foreach (var item in value)
+ {
+ await writer.WriteAsync(item, stream, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ protected override Task> ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ // Default to standard format (valueFlag = 0x00)
+ return ReadValueAsync(stream, reader, 0x00, cancellationToken);
+ }
+
+ ///
+ protected override async Task> ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ byte valueFlag, CancellationToken cancellationToken = default)
+ {
+ var length = (int)await reader.ReadNonNullableValueAsync(stream, cancellationToken).ConfigureAwait(false);
+ var result = new List();
+ var isBulked = (valueFlag & ValueFlagBulked) != 0;
+
+ for (var i = 0; i < length; i++)
+ {
+ var item = (TMember?)await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
+
+ if (isBulked)
+ {
+ var bulk = await stream.ReadLongAsync(cancellationToken).ConfigureAwait(false);
+ for (var j = 0; j < bulk; j++)
+ {
+ result.Add(item);
+ }
+ }
+ else
+ {
+ result.Add(item);
+ }
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/MapSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/MapSerializer.cs
new file mode 100644
index 00000000000..f8e9172d14b
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/MapSerializer.cs
@@ -0,0 +1,76 @@
+#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.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ ///
+ /// A generic dictionary serializer for GraphBinary 4.0.
+ ///
+ /// The type of keys in the dictionary.
+ /// The type of values in the dictionary.
+ public class MapSerializer : SimpleTypeSerializer> where TKey : notnull
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MapSerializer() : base(DataType.Map)
+ {
+ }
+
+ ///
+ protected override async Task WriteValueAsync(IDictionary value, Stream stream,
+ GraphBinaryWriter writer, CancellationToken cancellationToken = default)
+ {
+ await writer.WriteNonNullableValueAsync(value.Count, stream, cancellationToken).ConfigureAwait(false);
+
+ foreach (var key in value.Keys)
+ {
+ await writer.WriteAsync(key, stream, cancellationToken).ConfigureAwait(false);
+ await writer.WriteAsync(value[key], stream, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ protected override async Task> ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ var length = await stream.ReadIntAsync(cancellationToken).ConfigureAwait(false);
+ var result = new Dictionary(length);
+
+ for (var i = 0; i < length; i++)
+ {
+ var key = (TKey?) await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
+ if (key == null) throw new IOException("Read null as the key for a dictionary.");
+ var value = (TValue?) await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
+ result.Add(key, value);
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PathSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PathSerializer.cs
new file mode 100644
index 00000000000..03c6a260490
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PathSerializer.cs
@@ -0,0 +1,74 @@
+#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.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ ///
+ /// A serializer.
+ ///
+ public class PathSerializer : SimpleTypeSerializer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PathSerializer() : base(DataType.Path)
+ {
+ }
+
+ ///
+ protected override async Task WriteValueAsync(Path value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ await writer.WriteAsync(value.Labels, stream, cancellationToken).ConfigureAwait(false);
+ await writer.WriteAsync(value.Objects, stream, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ var readLabelObjects =
+ (List?)await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
+ if (readLabelObjects == null) throw new IOException("Read null, but expected a list of labels");
+ var labels = new List>();
+ foreach (var labelObjectList in readLabelObjects)
+ {
+ var labelSet = new HashSet();
+ foreach (var labelObj in (HashSet) labelObjectList)
+ {
+ labelSet.Add((string) labelObj);
+ }
+ labels.Add(labelSet);
+ }
+
+ var objects = (List?) await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
+ if (objects == null) throw new IOException("Read null, but expected a list of objects");
+ return new Path(labels, objects);
+ }
+ }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PropertySerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PropertySerializer.cs
new file mode 100644
index 00000000000..6eba38a2b60
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/PropertySerializer.cs
@@ -0,0 +1,68 @@
+#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;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ ///
+ /// A serializer.
+ ///
+ public class
+ PropertySerializer : SimpleTypeSerializer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public PropertySerializer() : base(DataType.Property)
+ {
+ }
+
+ ///
+ protected override async Task WriteValueAsync(Property value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ await writer.WriteNonNullableValueAsync(value.Key, stream, cancellationToken).ConfigureAwait(false);
+ await writer.WriteAsync(value.Value, stream, cancellationToken).ConfigureAwait(false);
+
+ // placeholder for the parent element
+ await writer.WriteAsync(null, stream, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ var p = new Property(
+ (string)await reader.ReadNonNullableValueAsync(stream, cancellationToken).ConfigureAwait(false),
+ await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false));
+
+ // discard parent element
+ await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
+
+ return p;
+ }
+ }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SetSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SetSerializer.cs
new file mode 100644
index 00000000000..c656ae706f5
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SetSerializer.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.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ ///
+ /// A generic set serializer for GraphBinary 4.0.
+ ///
+ /// The type of the set to serialize.
+ /// The type of elements in the set.
+ public class SetSerializer : SimpleTypeSerializer
+ where TSet : ISet, new()
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public SetSerializer() : base(DataType.Set)
+ {
+ }
+
+ ///
+ protected override async Task WriteValueAsync(TSet value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ var enumerable = (IEnumerable) value;
+ var list = enumerable.Cast().ToList();
+
+ await writer.WriteNonNullableValueAsync(list.Count, stream, cancellationToken).ConfigureAwait(false);
+
+ foreach (var item in list)
+ {
+ await writer.WriteAsync(item, stream, cancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ ///
+ protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ var length = (int)await reader.ReadNonNullableValueAsync(stream, cancellationToken).ConfigureAwait(false);
+ var result = new TSet();
+ for (var i = 0; i < length; i++)
+ {
+ result.Add((TMember?) await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false));
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SimpleTypeSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SimpleTypeSerializer.cs
new file mode 100644
index 00000000000..8115959560e
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SimpleTypeSerializer.cs
@@ -0,0 +1,143 @@
+#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;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ ///
+ /// Base class for serialization of types that don't contain type specific information only {type_code},
+ /// {value_flag} and {value}.
+ ///
+ /// The supported type.
+ public abstract class SimpleTypeSerializer : ITypeSerializer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ protected SimpleTypeSerializer(DataType dataType)
+ {
+ DataType = dataType;
+ }
+
+ ///
+ public DataType DataType { get; }
+
+ ///
+ public async Task WriteAsync(object? value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ await WriteNullableValueAsync((T?) value, stream, writer, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public async Task WriteNullableValueAsync(object? value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ if (value == null)
+ {
+ await writer.WriteValueFlagNullAsync(stream, cancellationToken).ConfigureAwait(false);
+ return;
+ }
+
+ await writer.WriteValueFlagNoneAsync(stream, cancellationToken).ConfigureAwait(false);
+
+ await WriteValueAsync((T) value, stream, writer, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public async Task WriteNonNullableValueAsync(object value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ await WriteValueAsync((T) value, stream, writer, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ /// Writes a non-nullable value into a stream.
+ ///
+ /// The value to write.
+ /// The stream to write to.
+ /// A .
+ /// The token to cancel the operation. The default value is None.
+ /// A task that represents the asynchronous write operation.
+ protected abstract Task WriteValueAsync(T value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default);
+
+ ///
+ public async Task ReadAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ return await ReadNullableValueAsync(stream, reader, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public async Task ReadNullableValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ var valueFlag = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+ if ((valueFlag & 1) == 1)
+ {
+ return null;
+ }
+
+ return await ReadValueAsync(stream, reader, valueFlag, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ public async Task ReadNonNullableValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ return (await ReadValueAsync(stream, reader, cancellationToken).ConfigureAwait(false))!;
+ }
+
+ ///
+ /// Reads a non-nullable value according to the type format with the value flag.
+ ///
+ /// The GraphBinary data to parse.
+ /// A .
+ /// The value flag byte that was read.
+ /// The token to cancel the operation. The default value is None.
+ /// The read value.
+ ///
+ /// Override this method to handle type-specific value flags (e.g., bulked format for List).
+ /// The default implementation delegates to .
+ ///
+ protected virtual Task ReadValueAsync(Stream stream, GraphBinaryReader reader, byte valueFlag,
+ CancellationToken cancellationToken = default)
+ {
+ return ReadValueAsync(stream, reader, cancellationToken);
+ }
+
+ ///
+ /// Reads a non-nullable value according to the type format.
+ ///
+ /// The GraphBinary data to parse.
+ /// A .
+ /// The token to cancel the operation. The default value is None.
+ /// The read value.
+ protected abstract Task ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default);
+ }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SingleTypeSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SingleTypeSerializer.cs
new file mode 100644
index 00000000000..6164581be57
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/SingleTypeSerializer.cs
@@ -0,0 +1,133 @@
+#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;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ ///
+ /// Provides serializers for types that can be represented as a single value and that can be read and write in a
+ /// single operation.
+ ///
+ public static class SingleTypeSerializers
+ {
+ ///
+ /// A serializer for values.
+ ///
+ public static readonly SingleTypeSerializer IntSerializer = new SingleTypeSerializer(DataType.Int,
+ (value, stream, cancellationToken) => stream.WriteIntAsync(value, cancellationToken),
+ (stream, cancellationToken) => stream.ReadIntAsync(cancellationToken));
+
+ ///
+ /// A serializer for values.
+ ///
+ public static readonly SingleTypeSerializer LongSerializer = new SingleTypeSerializer(DataType.Long,
+ (value, stream, cancellationToken) => stream.WriteLongAsync(value, cancellationToken),
+ (stream, cancellationToken) => stream.ReadLongAsync(cancellationToken));
+
+ ///
+ /// A serializer for values.
+ ///
+ public static readonly SingleTypeSerializer DoubleSerializer =
+ new SingleTypeSerializer(DataType.Double,
+ (value, stream, cancellationToken) => stream.WriteDoubleAsync(value, cancellationToken),
+ (stream, cancellationToken) => stream.ReadDoubleAsync(cancellationToken));
+
+ ///
+ /// A serializer for values.
+ ///
+ public static readonly SingleTypeSerializer FloatSerializer =
+ new SingleTypeSerializer(DataType.Float,
+ (value, stream, cancellationToken) => stream.WriteFloatAsync(value, cancellationToken),
+ (stream, cancellationToken) => stream.ReadFloatAsync(cancellationToken));
+
+ ///
+ /// A serializer for values.
+ ///
+ public static readonly SingleTypeSerializer ShortSerializer =
+ new SingleTypeSerializer(DataType.Short,
+ (value, stream, cancellationToken) => stream.WriteShortAsync(value, cancellationToken),
+ (stream, cancellationToken) => stream.ReadShortAsync(cancellationToken));
+
+ ///
+ /// A serializer for values.
+ ///
+ public static readonly SingleTypeSerializer BooleanSerializer =
+ new SingleTypeSerializer(DataType.Boolean,
+ (value, stream, cancellationToken) => stream.WriteBoolAsync(value, cancellationToken),
+ (stream, cancellationToken) => stream.ReadBoolAsync(cancellationToken));
+
+ ///
+ /// A serializer for values.
+ ///
+ public static readonly SingleTypeSerializer ByteSerializer = new SingleTypeSerializer(DataType.Byte,
+ (value, stream, cancellationToken) => stream.WriteSByteAsync(value, cancellationToken),
+ (stream, cancellationToken) => stream.ReadSByteAsync(cancellationToken));
+
+ ///
+ /// A serializer for values.
+ ///
+ public static readonly SingleTypeSerializer MarkerSerializer = new SingleTypeSerializer(DataType.Marker,
+ (value, stream, cancellationToken) => stream.WriteByteAsync(value.Value, cancellationToken),
+ async (stream, cancellationToken) =>
+ {
+ var markerValue = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+ return Marker.Of(markerValue);
+ });
+ }
+
+ ///
+ /// Represents a serializer for types that can be represented as a single value and that can be read and write in a
+ /// single operation.
+ ///
+ public class SingleTypeSerializer : SimpleTypeSerializer
+ {
+ private readonly Func _writeFunc;
+ private readonly Func> _readFunc;
+
+ internal SingleTypeSerializer(DataType dataType, Func writeFunc,
+ Func> readFunc)
+ : base(dataType)
+ {
+ _writeFunc = writeFunc;
+ _readFunc = readFunc;
+ }
+
+ ///
+ protected override async Task WriteValueAsync(T value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ await _writeFunc.Invoke(value, stream, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ return await _readFunc.Invoke(stream, cancellationToken).ConfigureAwait(false);
+ }
+ }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/StringSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/StringSerializer.cs
new file mode 100644
index 00000000000..73cce54aa5f
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/StringSerializer.cs
@@ -0,0 +1,62 @@
+#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.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ ///
+ /// A serializer.
+ ///
+ public class StringSerializer : SimpleTypeSerializer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public StringSerializer() : base(DataType.String)
+ {
+ }
+
+ ///
+ protected override async Task WriteValueAsync(string value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ var bytes = Encoding.UTF8.GetBytes(value);
+ await writer.WriteNonNullableValueAsync(bytes.Length, stream, cancellationToken).ConfigureAwait(false);
+ await stream.WriteAsync(bytes, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ var length = (int)await reader.ReadNonNullableValueAsync(stream, cancellationToken).ConfigureAwait(false);
+ var bytes = new byte[length];
+ await stream.ReadAsync(bytes, 0, length, cancellationToken).ConfigureAwait(false);
+ return Encoding.UTF8.GetString(bytes);
+ }
+ }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/UuidSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/UuidSerializer.cs
new file mode 100644
index 00000000000..dbecb308678
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/UuidSerializer.cs
@@ -0,0 +1,111 @@
+#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;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ ///
+ /// A serializer that serializes values as Uuid in GraphBinary.
+ ///
+ public class UuidSerializer : SimpleTypeSerializer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public UuidSerializer() : base(DataType.Uuid)
+ {
+ }
+
+ ///
+ protected override async Task WriteValueAsync(Guid value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ var bytes = value.ToByteArray();
+
+ // first 4 bytes in reverse order:
+ await stream.WriteByteAsync(bytes[3], cancellationToken).ConfigureAwait(false);
+ await stream.WriteByteAsync(bytes[2], cancellationToken).ConfigureAwait(false);
+ await stream.WriteByteAsync(bytes[1], cancellationToken).ConfigureAwait(false);
+ await stream.WriteByteAsync(bytes[0], cancellationToken).ConfigureAwait(false);
+
+ // 2 bytes in reverse order:
+ await stream.WriteByteAsync(bytes[5], cancellationToken).ConfigureAwait(false);
+ await stream.WriteByteAsync(bytes[4], cancellationToken).ConfigureAwait(false);
+
+ // 3 bytes in reverse order:
+ await stream.WriteByteAsync(bytes[7], cancellationToken).ConfigureAwait(false);
+ await stream.WriteByteAsync(bytes[6], cancellationToken).ConfigureAwait(false);
+
+ // 3 bytes:
+ await stream.WriteByteAsync(bytes[8], cancellationToken).ConfigureAwait(false);
+ await stream.WriteByteAsync(bytes[9], cancellationToken).ConfigureAwait(false);
+
+ // last 6 bytes:
+ await stream.WriteByteAsync(bytes[10], cancellationToken).ConfigureAwait(false);
+ await stream.WriteByteAsync(bytes[11], cancellationToken).ConfigureAwait(false);
+ await stream.WriteByteAsync(bytes[12], cancellationToken).ConfigureAwait(false);
+ await stream.WriteByteAsync(bytes[13], cancellationToken).ConfigureAwait(false);
+ await stream.WriteByteAsync(bytes[14], cancellationToken).ConfigureAwait(false);
+ await stream.WriteByteAsync(bytes[15], cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ var bytes = new byte[16];
+
+ // first 4 bytes in reverse order:
+ bytes[3] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+ bytes[2] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+ bytes[1] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+ bytes[0] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+
+ // 2 bytes in reverse order:
+ bytes[5] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+ bytes[4] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+
+ // 2 bytes in reverse order:
+ bytes[7] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+ bytes[6] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+
+ // 2 bytes:
+ bytes[8] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+ bytes[9] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+
+ // last 6 bytes:
+ bytes[10] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+ bytes[11] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+ bytes[12] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+ bytes[13] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+ bytes[14] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+ bytes[15] = await stream.ReadByteAsync(cancellationToken).ConfigureAwait(false);
+
+ return new Guid(bytes);
+ }
+ }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/VertexPropertySerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/VertexPropertySerializer.cs
new file mode 100644
index 00000000000..89f4acd767b
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/VertexPropertySerializer.cs
@@ -0,0 +1,78 @@
+#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.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ ///
+ /// A serializer for GraphBinary 4.0.
+ /// In v4, labels are serialized as List of Strings instead of a single String.
+ ///
+ public class VertexPropertySerializer : SimpleTypeSerializer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public VertexPropertySerializer() : base(DataType.VertexProperty)
+ {
+ }
+
+ ///
+ protected override async Task WriteValueAsync(VertexProperty value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ await writer.WriteAsync(value.Id, stream, cancellationToken).ConfigureAwait(false);
+ await writer.WriteNonNullableValueAsync(new List { value.Label }, stream, cancellationToken).ConfigureAwait(false);
+ await writer.WriteAsync(value.Value, stream, cancellationToken).ConfigureAwait(false);
+
+ // placeholder for the parent vertex
+ await writer.WriteAsync(null, stream, cancellationToken).ConfigureAwait(false);
+
+ // placeholder for properties
+ await writer.WriteAsync(null, stream, cancellationToken).ConfigureAwait(false);
+ }
+
+ ///
+ protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ var id = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
+ var labelList = (List)await reader.ReadNonNullableValueAsync>(stream, cancellationToken).ConfigureAwait(false);
+ var label = labelList.Count > 0 ? labelList[0] ?? "" : "";
+ var value = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
+
+ // discard the parent vertex
+ await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
+
+ var properties = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
+ var propertiesAsArray = null == properties ? Array.Empty() : (properties as List)?.ToArray();
+
+ return new VertexProperty(id, label, value, null, propertiesAsArray);
+ }
+ }
+}
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/VertexSerializer.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/VertexSerializer.cs
new file mode 100644
index 00000000000..9bad7be2d7a
--- /dev/null
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/Types/VertexSerializer.cs
@@ -0,0 +1,68 @@
+#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.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Gremlin.Net.Structure.IO.GraphBinary4.Types
+{
+ ///
+ /// A serializer for GraphBinary 4.0.
+ /// In v4, labels are serialized as List of Strings instead of a single String.
+ ///
+ public class VertexSerializer : SimpleTypeSerializer
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public VertexSerializer() : base(DataType.Vertex)
+ {
+ }
+
+ ///
+ protected override async Task WriteValueAsync(Vertex value, Stream stream, GraphBinaryWriter writer,
+ CancellationToken cancellationToken = default)
+ {
+ await writer.WriteAsync(value.Id, stream, cancellationToken).ConfigureAwait(false);
+ await writer.WriteNonNullableValueAsync(new List { value.Label }, stream, cancellationToken).ConfigureAwait(false);
+ await writer.WriteAsync(null, stream, cancellationToken).ConfigureAwait(false); // properties
+ }
+
+ ///
+ protected override async Task ReadValueAsync(Stream stream, GraphBinaryReader reader,
+ CancellationToken cancellationToken = default)
+ {
+ var id = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
+ var labelList = (List)await reader.ReadNonNullableValueAsync>(stream, cancellationToken).ConfigureAwait(false);
+ var label = labelList.Count > 0 ? labelList[0] ?? "" : "";
+
+ var properties = await reader.ReadAsync(stream, cancellationToken).ConfigureAwait(false);
+ var propertiesAsArray = null == properties ? Array.Empty() : (properties as List)?.ToArray();
+
+ return new Vertex(id, label, propertiesAsArray);
+ }
+ }
+}
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/GraphBinary4Tests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/GraphBinary4Tests.cs
new file mode 100644
index 00000000000..14d8a95e4d7
--- /dev/null
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/GraphBinary4Tests.cs
@@ -0,0 +1,794 @@
+#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.Collections.Generic;
+using System.Globalization;
+using System.IO;
+using System.Numerics;
+using System.Threading.Tasks;
+using Gremlin.Net.Process.Traversal;
+using Gremlin.Net.Structure;
+using Gremlin.Net.Structure.IO.GraphBinary4;
+using Xunit;
+using Path = Gremlin.Net.Structure.Path;
+
+namespace Gremlin.Net.UnitTest.Structure.IO.GraphBinary4
+{
+ public class GraphBinary4Tests
+ {
+ [Fact]
+ public async Task TestNull()
+ {
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(null, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Null(actual);
+ }
+
+ [Fact]
+ public async Task TestInt()
+ {
+ const int expected = 100;
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData(1, new byte[]{0x00, 0x00, 0x00, 0x01})]
+ [InlineData(257, new byte[]{0x00, 0x00, 0x01, 0x01})]
+ [InlineData(-1, new byte[]{0xFF, 0xFF, 0xFF, 0xFF})]
+ [InlineData(-2, new byte[]{0xFF, 0xFF, 0xFF, 0xFE})]
+ public async Task TestIntSpec(int value, byte[] expected)
+ {
+ var writer = CreateGraphBinaryWriter();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteNonNullableValueAsync(value, serializationStream);
+
+ var serBytes = serializationStream.ToArray();
+ Assert.Equal(expected, serBytes);
+ }
+
+ [Fact]
+ public async Task TestLong()
+ {
+ const long expected = 100;
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData(100.01f)]
+ [InlineData(float.NaN)]
+ [InlineData(float.NegativeInfinity)]
+ [InlineData(float.PositiveInfinity)]
+ public async Task TestFloat(float expected)
+ {
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestDouble()
+ {
+ const double expected = 100.001;
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestShort()
+ {
+ const short expected = 100;
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestDateTime()
+ {
+ var expected = DateTimeOffset.ParseExact("2016-12-14 16:14:36.295000", "yyyy-MM-dd HH:mm:ss.ffffff",
+ CultureInfo.InvariantCulture);
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestDateTimeWithNonUtcOffset()
+ {
+ // 2007-12-03T10:15:30+01:00 — the example from the spec
+ var expected = new DateTimeOffset(2007, 12, 3, 10, 15, 30, TimeSpan.FromHours(1));
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestDateTimeWithNegativeOffset()
+ {
+ var expected = new DateTimeOffset(2024, 6, 15, 8, 30, 0, TimeSpan.FromHours(-5));
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestMarkerEndOfStream()
+ {
+ var expected = Marker.EndOfStream;
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestMarkerEndOfStreamSpec()
+ {
+ var writer = CreateGraphBinaryWriter();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(Marker.EndOfStream, serializationStream);
+
+ Assert.Equal(new byte[] { 0xFD, 0x00, 0x00 }, serializationStream.ToArray());
+ }
+
+ [Fact]
+ public async Task TestString()
+ {
+ const string expected = "serialize this!";
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData("serialize this!", "serialize that!", "serialize that!", "stop telling me what to serialize")]
+ [InlineData(1, 2, 3, 4, 5)]
+ [InlineData(0.1, 1.1, 2.5, double.NaN)]
+ [InlineData(0.1f, 1.1f, 2.5f, float.NaN)]
+ public async Task TestHomogeneousList(params object[] listMembers)
+ {
+ var expected = new List(listMembers);
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestHomogeneousTypeSafeList()
+ {
+ var expected = new List {"test", "123"};
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestHeterogeneousList()
+ {
+ var expected = new List
+ {"serialize this!", 0, "serialize that!", "serialize that!", 1, "stop telling me what to serialize", 2};
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ ///
+ /// Tests the bulked list format (value_flag=0x02) where each item is followed by a Long bulk count.
+ /// The writer doesn't produce this format, so we construct the bytes manually to test the reader.
+ ///
+ [Fact]
+ public async Task TestBulkedList()
+ {
+ // Manually construct: type=List(0x12), value_flag=0x02 (bulked), count=2 items
+ // item 1: String "a", bulk=2 → ["a", "a"]
+ // item 2: String "b", bulk=1 → ["b"]
+ var stream = new MemoryStream();
+ var writer = CreateGraphBinaryWriter();
+ // type code + bulked flag
+ stream.WriteByte(DataType.List.TypeCode);
+ stream.WriteByte(0x02); // ValueFlagBulk
+ await writer.WriteNonNullableValueAsync(2, stream);
+ await writer.WriteAsync("a", stream);
+ await writer.WriteNonNullableValueAsync(2L, stream);
+ await writer.WriteAsync("b", stream);
+ await writer.WriteNonNullableValueAsync(1L, stream);
+
+ stream.Position = 0;
+ var reader = CreateGraphBinaryReader();
+ var actual = (List) await reader.ReadAsync(stream);
+
+ Assert.Equal(new List { "a", "a", "b" }, actual);
+ }
+
+ [Fact]
+ public async Task TestArray()
+ {
+ var expected = new string[] {"hallo", "welt"};
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData("serialize this!", "serialize that!", "serialize that!", "stop telling me what to serialize")]
+ [InlineData(1, 2, 3, 4, 5)]
+ [InlineData(0.1, 1.1, 2.5, double.NaN)]
+ [InlineData(0.1f, 1.1f, 2.5f, float.NaN)]
+ public async Task TestHomogeneousSet(params object[] listMembers)
+ {
+ var expected = new HashSet(listMembers);
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestHomogeneousTypeSafeSet()
+ {
+ var expected = new HashSet {"test", "123"};
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteNonNullableValueAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadNonNullableValueAsync>(serializationStream);
+
+ Assert.Equal(expected, actual);
+ Assert.Equal(expected.GetType(), actual.GetType());
+ }
+
+ [Fact]
+ public async Task TestHeterogeneousSet()
+ {
+ var expected = new HashSet
+ {"serialize this!", 0, "serialize that!", "serialize that!", 1, "stop telling me what to serialize", 2};
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestDictionary()
+ {
+ var expected = new Dictionary
+ {
+ {"yo", "what"},
+ {"go", "no!"},
+ {"number", 123},
+ {321, "crazy with the number for a key"},
+ {987, new List {"go", "deep", new Dictionary {{"here", "!"}}}}
+ };
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestHomogeneousTypeSafeDictionary()
+ {
+ var expected = new Dictionary
+ {
+ {"number", 123},
+ {"and", 456},
+ {"nothing else", 789}
+ };
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestHomogeneousTypeSafeDictionaryWithCorrectTyping()
+ {
+ var expected = new Dictionary
+ {
+ {"number", 123},
+ {"and", 456},
+ {"nothing else", 789}
+ };
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteNonNullableValueAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadNonNullableValueAsync>(serializationStream);
+
+ Assert.Equal(expected, actual);
+ Assert.Equal(expected.GetType(), actual.GetType());
+ }
+
+ [Fact]
+ public async Task TestGuid()
+ {
+ var expected = Guid.Parse("41d2e28a-20a4-4ab0-b379-d810dede3786");
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestGuidSerialization()
+ {
+ var toSerialize = Guid.Parse("00112233-4455-6677-8899-aabbccddeeff");
+ var writer = CreateGraphBinaryWriter();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteNonNullableValueAsync(toSerialize, serializationStream);
+
+ var expected = new byte[]
+ {0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff};
+ Assert.Equal(expected, serializationStream.ToArray());
+ }
+
+ [Fact]
+ public async Task TestVertex()
+ {
+ var expected = new Vertex(123, "person");
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task WriteNonNullableValueShouldThrowForNullValue()
+ {
+ var writer = CreateGraphBinaryWriter();
+ var serializationStream = new MemoryStream();
+
+ await Assert.ThrowsAsync(() => writer.WriteNonNullableValueAsync(null!, serializationStream));
+ }
+
+ [Fact]
+ public async Task TestEdge()
+ {
+ var expected = new Edge(123, new Vertex(1, "person"), "developed", new Vertex(10, "software"));
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestPath()
+ {
+ var expected =
+ new Path(
+ new List>
+ {new HashSet {"a", "b"}, new HashSet {"c", "d"}, new HashSet {"e"}},
+ new List {1, 2, 3});
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestProperty()
+ {
+ var expected = new Property("name", "stephen", null);
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestVertexProperty()
+ {
+ var expected = new VertexProperty(123, "name", "stephen", null);
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestDirection()
+ {
+ var expected = Direction.Out;
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestMerge()
+ {
+ var expected = Merge.OnCreate;
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestT()
+ {
+ var expected = T.Label;
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData(1)]
+ [InlineData(123)]
+ [InlineData(-1)]
+ [InlineData(-128)]
+ [InlineData(127)]
+ public async Task TestSByte(sbyte expected)
+ {
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData((sbyte)0, new byte[] { 0x00 })]
+ [InlineData((sbyte)1, new byte[] { 0x01 })]
+ [InlineData((sbyte)127, new byte[] { 0x7F })]
+ [InlineData((sbyte)-1, new byte[] { 0xFF })]
+ [InlineData((sbyte)-128, new byte[] { 0x80 })]
+ public async Task TestSByteSerializationSpec(sbyte value, byte[] expected)
+ {
+ var writer = CreateGraphBinaryWriter();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteNonNullableValueAsync(value, serializationStream);
+
+ var serBytes = serializationStream.ToArray();
+ Assert.Equal(expected, serBytes);
+ }
+
+ [Theory]
+ [InlineData((sbyte)0)]
+ [InlineData((sbyte)1)]
+ [InlineData((sbyte)127)]
+ [InlineData((sbyte)-1)]
+ [InlineData((sbyte)-128)]
+ [InlineData((sbyte)42)]
+ [InlineData((sbyte)-42)]
+ public async Task TestSByteRoundTrip(sbyte expected)
+ {
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ Assert.IsType(actual);
+ }
+
+ [Fact]
+ public async Task TestSByteMinMaxValues()
+ {
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+
+ // Test minimum value
+ var minStream = new MemoryStream();
+ await writer.WriteAsync(sbyte.MinValue, minStream);
+ minStream.Position = 0;
+ var actualMin = await reader.ReadAsync(minStream);
+ Assert.Equal(sbyte.MinValue, actualMin);
+
+ // Test maximum value
+ var maxStream = new MemoryStream();
+ await writer.WriteAsync(sbyte.MaxValue, maxStream);
+ maxStream.Position = 0;
+ var actualMax = await reader.ReadAsync(maxStream);
+ Assert.Equal(sbyte.MaxValue, actualMax);
+ }
+
+ [Fact]
+ public async Task TestBinary()
+ {
+ var expected = new byte[] {1, 2, 3};
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public async Task TestBoolean(bool expected)
+ {
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData('a')]
+ [InlineData('0')]
+ [InlineData('¢')]
+ [InlineData('€')]
+ public async Task TestChar(char expected)
+ {
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestDuration()
+ {
+ var expected = new TimeSpan(1, 2, 3, 4, 5);
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Fact]
+ public async Task TestBigInteger()
+ {
+ var expected = BigInteger.Parse("123456789987654321123456789987654321");
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ [Theory]
+ [InlineData("190.035")]
+ [InlineData("0.19")]
+ [InlineData("1900")]
+ [InlineData("-1900")]
+ [InlineData("100000000000000")]
+ [InlineData("100000000000000000000000000")]
+ public async Task TestBigDecimal(string decimalValue)
+ {
+ var expected = Decimal.Parse(decimalValue);
+ var writer = CreateGraphBinaryWriter();
+ var reader = CreateGraphBinaryReader();
+ var serializationStream = new MemoryStream();
+
+ await writer.WriteAsync(expected, serializationStream);
+ serializationStream.Position = 0;
+ var actual = await reader.ReadAsync(serializationStream);
+
+ Assert.Equal(expected, actual);
+ }
+
+ private static GraphBinaryWriter CreateGraphBinaryWriter()
+ {
+ return new GraphBinaryWriter();
+ }
+
+ private static GraphBinaryReader CreateGraphBinaryReader()
+ {
+ return new GraphBinaryReader();
+ }
+ }
+}
\ No newline at end of file
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/Model.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/Model.cs
new file mode 100644
index 00000000000..d749d6624e1
--- /dev/null
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/Model.cs
@@ -0,0 +1,210 @@
+#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.Collections.Generic;
+using System.Numerics;
+using Gremlin.Net.Process.Traversal;
+using Gremlin.Net.Structure;
+
+namespace Gremlin.Net.UnitTest.Structure.IO.GraphBinary4
+{
+ ///
+ /// Defines the supported types for GraphBinary 4.0 IO and provides test entries for round-trip testing.
+ ///
+ /// The following models aren't supported:
+ /// tinker-graph Graph type not implemented
+ /// traversal-tree Tree type not implemented
+ /// max-offsetdatetime DateTimeOffset.MaxValue exceeds serialization range
+ /// min-offsetdatetime DateTimeOffset.MinValue exceeds serialization range
+ /// forever-duration TimeSpan cannot represent Duration.FOREVER
+ /// pos-bigdecimal Java BigDecimal precision (33 digits) exceeds C# decimal (28-29 digits)
+ /// neg-bigdecimal Java BigDecimal precision (33 digits) exceeds C# decimal (28-29 digits)
+ /// var-type-map Dictionary doesn't support null key
+ ///
+ public static class Model
+ {
+ private static readonly Property SinceProperty = new("since", 2009, null);
+ private static readonly Property StartTime2005 = new("startTime", 2005, null);
+ private static readonly Property EndTime2005 = new("endTime", 2005, null);
+ private static readonly Property StartTime2001 = new("startTime", 2001, null);
+ private static readonly Property EndTime2004 = new("endTime", 2004, null);
+ private static readonly Property StartTime1997 = new("startTime", 1997, null);
+ private static readonly Property EndTime2001 = new("endTime", 2001, null);
+ private static readonly Property PropertyAB = new("a", "b", null);
+
+ private static readonly VertexProperty SantaFe = new(9, "location", "santa fe", null,
+ new dynamic[] { new Property("startTime", 2005, null) });
+ private static readonly VertexProperty Brussels = new(8, "location", "brussels", null,
+ new dynamic[] { StartTime2005, EndTime2005 });
+ private static readonly VertexProperty SantaCruz = new(7, "location", "santa cruz", null,
+ new dynamic[] { StartTime2001, EndTime2004 });
+ private static readonly VertexProperty SanDiego = new(6, "location", "san diego", null,
+ new dynamic[] { StartTime1997, EndTime2001 });
+ private static readonly VertexProperty NameMarko = new(0, "name", "marko", null);
+
+ public static Dictionary Entries { get; } = new()
+ {
+ // BigInteger
+ ["pos-biginteger"] = BigInteger.Parse("123456789987654321123456789987654321"),
+ ["neg-biginteger"] = BigInteger.Parse("-123456789987654321123456789987654321"),
+
+ // Byte (sbyte in C#)
+ ["min-byte"] = sbyte.MinValue, // -128
+ ["max-byte"] = sbyte.MaxValue, // 127
+
+ // Binary (byte[])
+ ["empty-binary"] = Array.Empty(),
+ ["str-binary"] = System.Text.Encoding.UTF8.GetBytes("some bytes for you"),
+
+ // Double
+ ["max-double"] = double.MaxValue,
+ ["min-double"] = double.Epsilon,
+ ["neg-max-double"] = -double.MaxValue,
+ ["neg-min-double"] = -double.Epsilon,
+ ["nan-double"] = double.NaN,
+ ["pos-inf-double"] = double.PositiveInfinity,
+ ["neg-inf-double"] = double.NegativeInfinity,
+ ["neg-zero-double"] = -0.0,
+
+ // Float
+ // Note: Java's Float.MIN_VALUE is the smallest positive float (equivalent to C#'s float.Epsilon),
+ // not the most negative float.
+ ["max-float"] = float.MaxValue,
+ ["min-float"] = float.Epsilon, // Java Float.MIN_VALUE = smallest positive float
+ ["neg-max-float"] = -float.MaxValue,
+ ["neg-min-float"] = -float.Epsilon, // negated Java Float.MIN_VALUE
+ ["nan-float"] = float.NaN,
+ ["pos-inf-float"] = float.PositiveInfinity,
+ ["neg-inf-float"] = float.NegativeInfinity,
+ ["neg-zero-float"] = -0.0f,
+
+ // Char
+ ["single-byte-char"] = 'a',
+ ["multi-byte-char"] = '\u03A9', // Greek capital letter Omega
+
+ // Null
+ ["unspecified-null"] = null,
+
+ // Boolean
+ ["true-boolean"] = true,
+ ["false-boolean"] = false,
+
+ // String
+ ["single-byte-string"] = "abc",
+ ["mixed-string"] = "abc\u0391\u0392\u0393", // abc + Greek letters Alpha, Beta, Gamma
+
+ // BulkSet (represented as List with duplicates)
+ ["var-bulklist"] = new List { "marko", "josh", "josh" },
+ ["empty-bulklist"] = new List(),
+
+ // Duration (TimeSpan)
+ ["zero-duration"] = TimeSpan.Zero,
+
+ // Edge
+ ["traversal-edge"] = new Edge(13, new Vertex(1, "person"), "develops", new Vertex(10, "software"),
+ new dynamic[] { SinceProperty }),
+ ["no-prop-edge"] = new Edge(13, new Vertex(1, "person"), "develops", new Vertex(10, "software")),
+
+ // Int
+ ["max-int"] = int.MaxValue,
+ ["min-int"] = int.MinValue,
+
+ // Long
+ ["max-long"] = long.MaxValue,
+ ["min-long"] = long.MinValue,
+
+ // List
+ ["var-type-list"] = new List { 1, "person", true, null },
+ ["empty-list"] = new List(),
+
+ // Map
+ ["var-type-map"] = new Dictionary
+ {
+ { "test", 123 },
+ { DateTimeOffset.FromUnixTimeMilliseconds(1481295), "red" },
+ { new List { 1, 2, 3 }, DateTimeOffset.FromUnixTimeMilliseconds(1481295) },
+ // Note: null key not supported in C# Dictionary
+ },
+ ["empty-map"] = new Dictionary(),
+
+ // Path
+ ["traversal-path"] = new Path(
+ new List> { new HashSet(), new HashSet(), new HashSet() },
+ new List { new Vertex(1, "person"), new Vertex(10, "software"), new Vertex(11, "software") }),
+ ["empty-path"] = new Path(new List>(), new List()),
+ ["prop-path"] = new Path(
+ new List> { new HashSet(), new HashSet(), new HashSet() },
+ new List
+ {
+ new Vertex(1, "person", new dynamic[]
+ {
+ new VertexProperty(123, "name", "stephen", null, new dynamic[]
+ {
+ new VertexProperty(0, "name", "marko", null),
+ new VertexProperty(6, "location", new List(), null)
+ })
+ }),
+ new Vertex(10, "software"),
+ new Vertex(11, "software")
+ }),
+
+ // Property
+ ["edge-property"] = new Property("since", 2009, null),
+ ["null-property"] = new Property("", null, null),
+
+ // Set
+ ["var-type-set"] = new HashSet { 2, "person", true, null },
+ ["empty-set"] = new HashSet(),
+
+ // Short
+ ["max-short"] = short.MaxValue,
+ ["min-short"] = short.MinValue,
+
+ // UUID (Guid)
+ ["specified-uuid"] = Guid.Parse("41d2e28a-20a4-4ab0-b379-d810dede3786"),
+ ["nil-uuid"] = Guid.Empty,
+
+ // Vertex
+ ["no-prop-vertex"] = new Vertex(1, "person"),
+ ["traversal-vertex"] = new Vertex(1, "person", new dynamic[]
+ {
+ new Property("name", NameMarko, null),
+ new Property("location", new List { SanDiego, SantaCruz, Brussels, SantaFe }, null)
+ }),
+
+ // VertexProperty
+ ["traversal-vertexproperty"] = new VertexProperty(0L, "name", "marko", null),
+ ["meta-vertexproperty"] = new VertexProperty(1, "person", "stephen", null,
+ new dynamic[] { PropertyAB }),
+ ["set-cardinality-vertexproperty"] = new VertexProperty(1, "person",
+ new HashSet { "stephen", "marko" }, null, new dynamic[] { PropertyAB }),
+
+ // T enum
+ ["id-t"] = T.Id,
+
+ // Direction enum
+ ["out-direction"] = Direction.Out,
+ };
+ }
+}
diff --git a/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/RoundTripTests.cs b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/RoundTripTests.cs
new file mode 100644
index 00000000000..c270105fa24
--- /dev/null
+++ b/gremlin-dotnet/test/Gremlin.Net.UnitTest/Structure/IO/GraphBinary4/RoundTripTests.cs
@@ -0,0 +1,364 @@
+#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.GraphBinary4;
+using Xunit;
+
+namespace Gremlin.Net.UnitTest.Structure.IO.GraphBinary4
+{
+ ///
+ /// Round trip testing of GraphBinary 4.0 compared to a correct "model".
+ /// Set the IO_TEST_DIRECTORY environment variable to the directory where
+ /// the .gbin files that represent the serialized "model" are located.
+ ///
+ public class RoundTripTests
+ {
+ private const string GremlinTestDir = "gremlin-test/src/main/resources/org/apache/tinkerpop/gremlin/structure/io/graphbinary/";
+ private const string DirectorySearchPattern = "gremlin-dotnet";
+
+ private static readonly string TestResourceDirectory;
+ private static readonly GraphBinaryWriter Writer = new();
+ private static readonly GraphBinaryReader Reader = new();
+
+ static RoundTripTests()
+ {
+ var envDir = Environment.GetEnvironmentVariable("IO_TEST_DIRECTORY");
+ if (!string.IsNullOrEmpty(envDir))
+ {
+ TestResourceDirectory = envDir;
+ }
+ else
+ {
+ // Find the root directory by searching for gremlin-dotnet in the current path
+ var currentDir = AppDomain.CurrentDomain.BaseDirectory;
+ var index = currentDir.IndexOf(DirectorySearchPattern, StringComparison.Ordinal);
+ var defaultDir = index >= 0 ? currentDir[..index] : currentDir;
+ TestResourceDirectory = Path.Combine(defaultDir, GremlinTestDir);
+ }
+ }
+
+ private static object? GetEntry(string title) => Model.Entries[title];
+
+ private static byte[] ReadFileByName(string resourceName)
+ {
+ var fullName = Path.Combine(TestResourceDirectory, $"{resourceName}-v4.gbin");
+ return File.ReadAllBytes(fullName);
+ }
+
+ ///
+ /// Runs the regular set of tests for the type:
+ /// 1. model to deserialized object
+ /// 2. written bytes to read bytes
+ /// 3. round tripped (read then written) bytes to read bytes
+ ///
+ private async Task Run(string resourceName, Func? comparator = null)
+ {
+ var resourceBytes = ReadFileByName(resourceName);
+ var model = GetEntry(resourceName);
+
+ using var readStream = new MemoryStream(resourceBytes);
+ var read = await Reader.ReadAsync(readStream);
+
+ if (comparator != null)
+ Assert.True(comparator(model, read));
+ else
+ Assert.Equal(model, read);
+
+ using var writeStream = new MemoryStream();
+ await Writer.WriteAsync(model, writeStream);
+ Assert.Equal(resourceBytes, writeStream.ToArray());
+
+ using var roundTripReadStream = new MemoryStream(resourceBytes);
+ var roundTripRead = await Reader.ReadAsync(roundTripReadStream);
+ using var roundTripWriteStream = new MemoryStream();
+ await Writer.WriteAsync(roundTripRead, roundTripWriteStream);
+ Assert.Equal(resourceBytes, roundTripWriteStream.ToArray());
+ }
+
+ ///
+ /// Runs the read test which compares the model to deserialized object.
+ /// This should only be used in cases where there is only a deserializer
+ /// but no serializer for the same type.
+ ///
+ private async Task RunRead(string resourceName, Func? comparator = null)
+ {
+ var resourceBytes = ReadFileByName(resourceName);
+ var model = GetEntry(resourceName);
+
+ using var readStream = new MemoryStream(resourceBytes);
+ var read = await Reader.ReadAsync(readStream);
+
+ if (comparator != null)
+ Assert.True(comparator(model, read));
+ else
+ Assert.Equal(model, read);
+ }
+
+ ///
+ /// Runs a reduced set of tests for the type:
+ /// 1. model to deserialized object
+ /// 2. model to round tripped (written then read) object
+ /// Use this in cases where the regular Run() function is too stringent.
+ /// E.g. when ordering doesn't matter like for sets.
+ ///
+ private async Task RunWriteRead(string resourceName, Func? comparator = null)
+ {
+ var resourceBytes = ReadFileByName(resourceName);
+ var model = GetEntry(resourceName);
+
+ using var readStream = new MemoryStream(resourceBytes);
+ var read = await Reader.ReadAsync(readStream);
+
+ using var writeStream = new MemoryStream();
+ await Writer.WriteAsync(model, writeStream);
+ using var roundTripStream = new MemoryStream(writeStream.ToArray());
+ var roundTripped = await Reader.ReadAsync(roundTripStream);
+
+ if (comparator != null)
+ {
+ Assert.True(comparator(model, read));
+ Assert.True(comparator(model, roundTripped));
+ }
+ else
+ {
+ Assert.Equal(model, read);
+ Assert.Equal(model, roundTripped);
+ }
+ }
+
+ private static bool NanComparator(object? x, object? y)
+ {
+ return x switch
+ {
+ double dx when y is double dy => double.IsNaN(dx) && double.IsNaN(dy),
+ float fx when y is float fy => float.IsNaN(fx) && float.IsNaN(fy),
+ _ => false
+ };
+ }
+
+ // BigInteger tests
+ [Fact]
+ public Task TestPosBigInteger() => Run("pos-biginteger");
+
+ [Fact]
+ public Task TestNegBigInteger() => Run("neg-biginteger");
+
+ // Byte tests
+ [Fact]
+ public Task TestMinByte() => Run("min-byte");
+
+ [Fact]
+ public Task TestMaxByte() => Run("max-byte");
+
+ // Binary tests
+ [Fact]
+ public Task TestEmptyBinary() => Run("empty-binary");
+
+ [Fact]
+ public Task TestStrBinary() => Run("str-binary");
+
+ // Double tests
+ [Fact]
+ public Task TestMaxDouble() => Run("max-double");
+
+ [Fact]
+ public Task TestMinDouble() => Run("min-double");
+
+ [Fact]
+ public Task TestNegMaxDouble() => Run("neg-max-double");
+
+ [Fact]
+ public Task TestNegMinDouble() => Run("neg-min-double");
+
+ [Fact]
+ public Task TestNanDouble() => RunWriteRead("nan-double", NanComparator); // C# has different NaN representation
+
+ [Fact]
+ public Task TestPosInfDouble() => Run("pos-inf-double");
+
+ [Fact]
+ public Task TestNegInfDouble() => Run("neg-inf-double");
+
+ [Fact]
+ public Task TestNegZeroDouble() => Run("neg-zero-double");
+
+ // Float tests
+ [Fact]
+ public Task TestMaxFloat() => Run("max-float");
+
+ [Fact]
+ public Task TestMinFloat() => Run("min-float");
+
+ [Fact]
+ public Task TestNegMaxFloat() => Run("neg-max-float");
+
+ [Fact]
+ public Task TestNegMinFloat() => Run("neg-min-float");
+
+ [Fact]
+ public Task TestNanFloat() => RunWriteRead("nan-float", NanComparator); // C# has different NaN representation
+
+ [Fact]
+ public Task TestPosInfFloat() => Run("pos-inf-float");
+
+ [Fact]
+ public Task TestNegInfFloat() => Run("neg-inf-float");
+
+ [Fact]
+ public Task TestNegZeroFloat() => Run("neg-zero-float");
+
+ // Char tests
+ [Fact]
+ public Task TestSingleByteChar() => Run("single-byte-char");
+
+ [Fact]
+ public Task TestMultiByteChar() => Run("multi-byte-char");
+
+ // Null test
+ [Fact]
+ public Task TestUnspecifiedNull() => Run("unspecified-null");
+
+ // Boolean tests
+ [Fact]
+ public Task TestTrueBoolean() => Run("true-boolean");
+
+ [Fact]
+ public Task TestFalseBoolean() => Run("false-boolean");
+
+ // String tests
+ [Fact]
+ public Task TestSingleByteString() => Run("single-byte-string");
+
+ [Fact]
+ public Task TestMixedString() => Run("mixed-string");
+
+ // BulkSet tests (read only - BulkSet deserialized as List)
+ [Fact]
+ public Task TestVarBulkList() => RunRead("var-bulklist");
+
+ [Fact]
+ public Task TestEmptyBulkList() => RunRead("empty-bulklist");
+
+ // Duration tests
+ [Fact]
+ public Task TestZeroDuration() => Run("zero-duration");
+
+ // Edge tests
+ [Fact]
+ public Task TestTraversalEdge() => RunWriteRead("traversal-edge"); // properties aren't serialized
+
+ [Fact]
+ public Task TestNoPropEdge() => RunWriteRead("no-prop-edge"); // properties written as null not empty list
+
+ // Int tests
+ [Fact]
+ public Task TestMaxInt() => Run("max-int");
+
+ [Fact]
+ public Task TestMinInt() => Run("min-int");
+
+ // Long tests
+ [Fact]
+ public Task TestMaxLong() => Run("max-long");
+
+ [Fact]
+ public Task TestMinLong() => Run("min-long");
+
+ // List tests
+ [Fact]
+ public Task TestVarTypeList() => Run("var-type-list");
+
+ [Fact]
+ public Task TestEmptyList() => Run("empty-list");
+
+ // Map tests
+ [Fact]
+ public Task TestEmptyMap() => Run("empty-map");
+
+ // Path tests
+ [Fact]
+ public Task TestTraversalPath() => RunWriteRead("traversal-path"); // properties written as null not empty list
+
+ [Fact]
+ public Task TestEmptyPath() => Run("empty-path");
+
+ [Fact]
+ public Task TestPropPath() => RunWriteRead("prop-path"); // properties aren't serialized
+
+ // Property tests
+ [Fact]
+ public Task TestEdgeProperty() => Run("edge-property");
+
+ [Fact]
+ public Task TestNullProperty() => Run("null-property");
+
+ // Set tests
+ [Fact]
+ public Task TestVarTypeSet() => RunWriteRead("var-type-set"); // order not guaranteed in HashSet
+
+ [Fact]
+ public Task TestEmptySet() => Run("empty-set");
+
+ // Short tests
+ [Fact]
+ public Task TestMaxShort() => Run("max-short");
+
+ [Fact]
+ public Task TestMinShort() => Run("min-short");
+
+ // UUID tests
+ [Fact]
+ public Task TestSpecifiedUuid() => Run("specified-uuid");
+
+ [Fact]
+ public Task TestNilUuid() => Run("nil-uuid");
+
+ // Vertex tests
+ [Fact]
+ public Task TestNoPropVertex() => RunWriteRead("no-prop-vertex"); // properties written as null not empty list
+
+ [Fact]
+ public Task TestTraversalVertex() => RunWriteRead("traversal-vertex"); // properties aren't serialized
+
+ // VertexProperty tests
+ [Fact]
+ public Task TestTraversalVertexProperty() => RunWriteRead("traversal-vertexproperty"); // properties aren't serialized
+
+ [Fact]
+ public Task TestMetaVertexProperty() => RunWriteRead("meta-vertexproperty"); // properties aren't serialized
+
+ [Fact]
+ public Task TestSetCardinalityVertexProperty() => RunWriteRead("set-cardinality-vertexproperty"); // properties aren't serialized
+
+ // T enum test
+ [Fact]
+ public Task TestIdT() => Run("id-t");
+
+ // Direction enum test
+ [Fact]
+ public Task TestOutDirection() => Run("out-direction");
+ }
+}
From 1b3c1fa4da8291a06e509914f0d8f25b244df777 Mon Sep 17 00:00:00 2001
From: Ken Hu <106191785+kenhuuu@users.noreply.github.com>
Date: Wed, 4 Mar 2026 15:04:55 -0800
Subject: [PATCH 2/2] Fix incorrect typecode for Merge
---
.../src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs
index 8ae3199f163..9490a34302e 100644
--- a/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs
+++ b/gremlin-dotnet/src/Gremlin.Net/Structure/IO/GraphBinary4/DataType.cs
@@ -50,7 +50,6 @@ public class DataType : IEquatable
public static readonly DataType VertexProperty = new DataType(0x12);
public static readonly DataType Direction = new DataType(0x18);
public static readonly DataType T = new DataType(0x20);
- public static readonly DataType Merge = new DataType(0x21);
public static readonly DataType BigDecimal = new DataType(0x22);
public static readonly DataType BigInteger = new DataType(0x23);
public static readonly DataType Byte = new DataType(0x24);
@@ -59,6 +58,8 @@ public class DataType : IEquatable
public static readonly DataType Boolean = new DataType(0x27);
// Not yet implemented
// public static readonly DataType Tree = new DataType(0x2B);
+ public static readonly DataType Merge = new DataType(0x2E);
+ // Not yet implemented
// public static readonly DataType CompositePDT = new DataType(0xF0);
// public static readonly DataType PrimitivePDT = new DataType(0xF1);
public static readonly DataType Char = new DataType(0x80);