diff --git a/specifications/bson-binary-vector/tests/README.md b/specifications/bson-binary-vector/tests/README.md
new file mode 100644
index 00000000000..3357ebb2644
--- /dev/null
+++ b/specifications/bson-binary-vector/tests/README.md
@@ -0,0 +1,58 @@
+# Testing Binary subtype 9: Vector
+
+The JSON files in this directory tree are platform-independent tests that drivers can use to prove their conformance to
+the specification.
+
+These tests focus on the roundtrip of the list of numbers as input/output, along with their data type and byte padding.
+
+Additional tests exist in `bson_corpus/tests/binary.json` but do not sufficiently test the end-to-end process of Vector
+to BSON. For this reason, drivers must create a bespoke test runner for the vector subtype.
+
+## Format
+
+The test data corpus consists of a JSON file for each data type (dtype). Each file contains a number of test cases,
+under the top-level key "tests". Each test case pertains to a single vector. The keys provide the specification of the
+vector. Valid cases also include the Canonical BSON format of a document {test_key: binary}. The "test_key" is common,
+and specified at the top level.
+
+#### Top level keys
+
+Each JSON file contains three top-level keys.
+
+- `description`: human-readable description of what is in the file
+- `test_key`: name used for key when encoding/decoding a BSON document containing the single BSON Binary for the test
+ case. Applies to *every* case.
+- `tests`: array of test case objects, each of which have the following keys. Valid cases will also contain additional
+ binary and json encoding values.
+
+#### Keys of individual tests cases
+
+- `description`: string describing the test.
+- `valid`: boolean indicating if the vector, dtype, and padding should be considered a valid input.
+- `vector`: list of numbers
+- `dtype_hex`: string defining the data type in hex (e.g. "0x10", "0x27")
+- `dtype_alias`: (optional) string defining the data dtype, perhaps as Enum.
+- `padding`: (optional) integer for byte padding. Defaults to 0.
+- `canonical_bson`: (required if valid is true) an (uppercase) big-endian hex representation of a BSON byte string.
+
+## Required tests
+
+#### To prove correct in a valid case (`valid: true`), one MUST
+
+- encode a document from the numeric values, dtype, and padding, along with the "test_key", and assert this matches the
+ canonical_bson string.
+- decode the canonical_bson into its binary form, and then assert that the numeric values, dtype, and padding all match
+ those provided in the JSON.
+
+Note: For floating point number types, exact numerical matches may not be possible. Drivers that natively support the
+floating-point type being tested (e.g., when testing float32 vector values in a driver that natively supports float32),
+MUST assert that the input float array is the same after encoding and decoding.
+
+#### To prove correct in an invalid case (`valid:false`), one MUST
+
+- raise an exception when attempting to encode a document from the numeric values, dtype, and padding.
+
+## FAQ
+
+- What MongoDB Server version does this apply to?
+ - Files in the "specifications" repository have no version scheme. They are not tied to a MongoDB server version.
diff --git a/specifications/bson-binary-vector/tests/float32.json b/specifications/bson-binary-vector/tests/float32.json
new file mode 100644
index 00000000000..872c435323d
--- /dev/null
+++ b/specifications/bson-binary-vector/tests/float32.json
@@ -0,0 +1,51 @@
+{
+ "description": "Tests of Binary subtype 9, Vectors, with dtype FLOAT32",
+ "test_key": "vector",
+ "tests": [
+ {
+ "description": "Simple Vector FLOAT32",
+ "valid": true,
+ "vector": [127.0, 7.0],
+ "dtype_hex": "0x27",
+ "dtype_alias": "FLOAT32",
+ "padding": 0,
+ "canonical_bson": "1C00000005766563746F72000A0000000927000000FE420000E04000"
+ },
+ {
+ "description": "Vector with decimals and negative value FLOAT32",
+ "valid": true,
+ "vector": [127.7, -7.7],
+ "dtype_hex": "0x27",
+ "dtype_alias": "FLOAT32",
+ "padding": 0,
+ "canonical_bson": "1C00000005766563746F72000A0000000927006666FF426666F6C000"
+ },
+ {
+ "description": "Empty Vector FLOAT32",
+ "valid": true,
+ "vector": [],
+ "dtype_hex": "0x27",
+ "dtype_alias": "FLOAT32",
+ "padding": 0,
+ "canonical_bson": "1400000005766563746F72000200000009270000"
+ },
+ {
+ "description": "Infinity Vector FLOAT32",
+ "valid": true,
+ "vector": ["-inf", 0.0, "inf"],
+ "dtype_hex": "0x27",
+ "dtype_alias": "FLOAT32",
+ "padding": 0,
+ "canonical_bson": "2000000005766563746F72000E000000092700000080FF000000000000807F00"
+ },
+ {
+ "description": "FLOAT32 with padding",
+ "valid": false,
+ "vector": [127.0, 7.0],
+ "dtype_hex": "0x27",
+ "dtype_alias": "FLOAT32",
+ "padding": 3
+ }
+ ]
+}
+
diff --git a/specifications/bson-binary-vector/tests/int8.json b/specifications/bson-binary-vector/tests/int8.json
new file mode 100644
index 00000000000..7529721e5ea
--- /dev/null
+++ b/specifications/bson-binary-vector/tests/int8.json
@@ -0,0 +1,57 @@
+{
+ "description": "Tests of Binary subtype 9, Vectors, with dtype INT8",
+ "test_key": "vector",
+ "tests": [
+ {
+ "description": "Simple Vector INT8",
+ "valid": true,
+ "vector": [127, 7],
+ "dtype_hex": "0x03",
+ "dtype_alias": "INT8",
+ "padding": 0,
+ "canonical_bson": "1600000005766563746F7200040000000903007F0700"
+ },
+ {
+ "description": "Empty Vector INT8",
+ "valid": true,
+ "vector": [],
+ "dtype_hex": "0x03",
+ "dtype_alias": "INT8",
+ "padding": 0,
+ "canonical_bson": "1400000005766563746F72000200000009030000"
+ },
+ {
+ "description": "Overflow Vector INT8",
+ "valid": false,
+ "vector": [128],
+ "dtype_hex": "0x03",
+ "dtype_alias": "INT8",
+ "padding": 0
+ },
+ {
+ "description": "Underflow Vector INT8",
+ "valid": false,
+ "vector": [-129],
+ "dtype_hex": "0x03",
+ "dtype_alias": "INT8",
+ "padding": 0
+ },
+ {
+ "description": "INT8 with padding",
+ "valid": false,
+ "vector": [127, 7],
+ "dtype_hex": "0x03",
+ "dtype_alias": "INT8",
+ "padding": 3
+ },
+ {
+ "description": "INT8 with float inputs",
+ "valid": false,
+ "vector": [127.77, 7.77],
+ "dtype_hex": "0x03",
+ "dtype_alias": "INT8",
+ "padding": 0
+ }
+ ]
+}
+
diff --git a/specifications/bson-binary-vector/tests/packed_bit.json b/specifications/bson-binary-vector/tests/packed_bit.json
new file mode 100644
index 00000000000..035776e87f7
--- /dev/null
+++ b/specifications/bson-binary-vector/tests/packed_bit.json
@@ -0,0 +1,98 @@
+{
+ "description": "Tests of Binary subtype 9, Vectors, with dtype PACKED_BIT",
+ "test_key": "vector",
+ "tests": [
+ {
+ "description": "Padding specified with no vector data PACKED_BIT",
+ "valid": false,
+ "vector": [],
+ "dtype_hex": "0x10",
+ "dtype_alias": "PACKED_BIT",
+ "padding": 1
+ },
+ {
+ "description": "Simple Vector PACKED_BIT",
+ "valid": true,
+ "vector": [127, 7],
+ "dtype_hex": "0x10",
+ "dtype_alias": "PACKED_BIT",
+ "padding": 0,
+ "canonical_bson": "1600000005766563746F7200040000000910007F0700"
+ },
+ {
+ "description": "Empty Vector PACKED_BIT",
+ "valid": true,
+ "vector": [],
+ "dtype_hex": "0x10",
+ "dtype_alias": "PACKED_BIT",
+ "padding": 0,
+ "canonical_bson": "1400000005766563746F72000200000009100000"
+ },
+ {
+ "description": "PACKED_BIT with padding",
+ "valid": true,
+ "vector": [127, 7],
+ "dtype_hex": "0x10",
+ "dtype_alias": "PACKED_BIT",
+ "padding": 3,
+ "canonical_bson": "1600000005766563746F7200040000000910037F0700"
+ },
+ {
+ "description": "Overflow Vector PACKED_BIT",
+ "valid": false,
+ "vector": [256],
+ "dtype_hex": "0x10",
+ "dtype_alias": "PACKED_BIT",
+ "padding": 0
+ },
+ {
+ "description": "Underflow Vector PACKED_BIT",
+ "valid": false,
+ "vector": [-1],
+ "dtype_hex": "0x10",
+ "dtype_alias": "PACKED_BIT",
+ "padding": 0
+ },
+ {
+ "description": "Vector with float values PACKED_BIT",
+ "valid": false,
+ "vector": [127.5],
+ "dtype_hex": "0x10",
+ "dtype_alias": "PACKED_BIT",
+ "padding": 0
+ },
+ {
+ "description": "Padding specified with no vector data PACKED_BIT",
+ "valid": false,
+ "vector": [],
+ "dtype_hex": "0x10",
+ "dtype_alias": "PACKED_BIT",
+ "padding": 1
+ },
+ {
+ "description": "Exceeding maximum padding PACKED_BIT",
+ "valid": false,
+ "vector": [1],
+ "dtype_hex": "0x10",
+ "dtype_alias": "PACKED_BIT",
+ "padding": 8
+ },
+ {
+ "description": "Negative padding PACKED_BIT",
+ "valid": false,
+ "vector": [1],
+ "dtype_hex": "0x10",
+ "dtype_alias": "PACKED_BIT",
+ "padding": -1
+ },
+ {
+ "description": "Vector with float values PACKED_BIT",
+ "valid": false,
+ "vector": [127.5],
+ "dtype_hex": "0x10",
+ "dtype_alias": "PACKED_BIT",
+ "padding": 0
+ }
+ ]
+}
+
diff --git a/specifications/bson-corpus/tests/binary.json b/specifications/bson-corpus/tests/binary.json
index 20aaef743b0..0e0056f3a2c 100644
--- a/specifications/bson-corpus/tests/binary.json
+++ b/specifications/bson-corpus/tests/binary.json
@@ -74,6 +74,36 @@
"description": "$type query operator (conflicts with legacy $binary form with $type field)",
"canonical_bson": "180000000378001000000010247479706500020000000000",
"canonical_extjson": "{\"x\" : { \"$type\" : {\"$numberInt\": \"2\"}}}"
+ },
+ {
+ "description": "subtype 0x09 Vector FLOAT32",
+ "canonical_bson": "170000000578000A0000000927000000FE420000E04000",
+ "canonical_extjson": "{\"x\": {\"$binary\": {\"base64\": \"JwAAAP5CAADgQA==\", \"subType\": \"09\"}}}"
+ },
+ {
+ "description": "subtype 0x09 Vector INT8",
+ "canonical_bson": "11000000057800040000000903007F0700",
+ "canonical_extjson": "{\"x\": {\"$binary\": {\"base64\": \"AwB/Bw==\", \"subType\": \"09\"}}}"
+ },
+ {
+ "description": "subtype 0x09 Vector PACKED_BIT",
+ "canonical_bson": "11000000057800040000000910007F0700",
+ "canonical_extjson": "{\"x\": {\"$binary\": {\"base64\": \"EAB/Bw==\", \"subType\": \"09\"}}}"
+ },
+ {
+ "description": "subtype 0x09 Vector (Zero-length) FLOAT32",
+ "canonical_bson": "0F0000000578000200000009270000",
+ "canonical_extjson": "{\"x\": {\"$binary\": {\"base64\": \"JwA=\", \"subType\": \"09\"}}}"
+ },
+ {
+ "description": "subtype 0x09 Vector (Zero-length) INT8",
+ "canonical_bson": "0F0000000578000200000009030000",
+ "canonical_extjson": "{\"x\": {\"$binary\": {\"base64\": \"AwA=\", \"subType\": \"09\"}}}"
+ },
+ {
+ "description": "subtype 0x09 Vector (Zero-length) PACKED_BIT",
+ "canonical_bson": "0F0000000578000200000009100000",
+ "canonical_extjson": "{\"x\": {\"$binary\": {\"base64\": \"EAA=\", \"subType\": \"09\"}}}"
}
],
"decodeErrors": [
diff --git a/src/MongoDB.Bson/ObjectModel/BsonBinarySubType.cs b/src/MongoDB.Bson/ObjectModel/BsonBinarySubType.cs
index 6fbde665ee4..67c019b5881 100644
--- a/src/MongoDB.Bson/ObjectModel/BsonBinarySubType.cs
+++ b/src/MongoDB.Bson/ObjectModel/BsonBinarySubType.cs
@@ -60,6 +60,10 @@ public enum BsonBinarySubType
///
Sensitive = 0x08,
///
+ /// Vector data.
+ ///
+ Vector = 0x09,
+ ///
/// User defined binary data.
///
UserDefined = 0x80
diff --git a/src/MongoDB.Bson/ObjectModel/BsonVector.cs b/src/MongoDB.Bson/ObjectModel/BsonVector.cs
new file mode 100644
index 00000000000..e5cb8ac1a7f
--- /dev/null
+++ b/src/MongoDB.Bson/ObjectModel/BsonVector.cs
@@ -0,0 +1,102 @@
+/* Copyright 2010-present MongoDB Inc.
+*
+* Licensed 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.
+*/
+
+using System;
+using MongoDB.Bson.ObjectModel;
+
+namespace MongoDB.Bson
+{
+ ///
+ /// Represents a BSON vector.
+ ///
+ public abstract class BsonVectorBase
+ where TItem : struct
+ {
+ ///
+ /// Initializes a new instance of the BsonVector class.
+ ///
+ protected BsonVectorBase(ReadOnlyMemory data, BsonVectorDataType dataType)
+ {
+ DataType = dataType;
+ Data = data;
+ }
+
+ ///
+ /// Gets the vector data type.
+ ///
+ public BsonVectorDataType DataType { get; }
+
+ ///
+ /// Gets the vector data.
+ ///
+ public ReadOnlyMemory Data { get; }
+ }
+
+ ///
+ /// Represents a vector of values.
+ ///
+ public sealed class BsonVectorFloat32 : BsonVectorBase
+ {
+ ///
+ /// Initializes a new instance of the BsonVectorFloat32 class.
+ ///
+ public BsonVectorFloat32(ReadOnlyMemory data) : base(data, BsonVectorDataType.Float32)
+ {
+ }
+ }
+
+ ///
+ /// Represents a vector of values.
+ ///
+ public sealed class BsonVectorInt8 : BsonVectorBase
+ {
+ ///
+ /// Initializes a new instance of the BsonVectorInt8 class.
+ ///
+ public BsonVectorInt8(ReadOnlyMemory data) : base(data, BsonVectorDataType.Int8)
+ {
+ }
+ }
+
+ ///
+ /// Represents a vector of 0/1 values.
+ /// The vector values are packed into groups of 8 (a byte).
+ ///
+ public sealed class BsonVectorPackedBit : BsonVectorBase
+ {
+ ///
+ /// Initializes a new instance of the BsonVectorPackedBit class.
+ ///
+ public BsonVectorPackedBit(ReadOnlyMemory data, byte padding) : base(data, BsonVectorDataType.PackedBit)
+ {
+ if (padding < 0 || padding > 7)
+ {
+ throw new ArgumentOutOfRangeException(nameof(padding), padding, "Padding is expected to be in the range of [0..7].");
+ }
+
+ if (padding > 0 && data.Length == 0)
+ {
+ throw new ArgumentException("Can't specify non zero padding with no data.");
+ }
+
+ Padding = padding;
+ }
+
+ ///
+ /// Gets the bits padding.
+ ///
+ public byte Padding { get; }
+ }
+}
diff --git a/src/MongoDB.Bson/ObjectModel/BsonVectorDataType.cs b/src/MongoDB.Bson/ObjectModel/BsonVectorDataType.cs
new file mode 100644
index 00000000000..73b06d66c76
--- /dev/null
+++ b/src/MongoDB.Bson/ObjectModel/BsonVectorDataType.cs
@@ -0,0 +1,39 @@
+/* Copyright 2010-present MongoDB Inc.
+*
+* Licensed 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.
+*/
+
+namespace MongoDB.Bson.ObjectModel
+{
+ ///
+ /// Represents the data type of BSON Vector.
+ ///
+ ///
+ public enum BsonVectorDataType
+ {
+ ///
+ /// The float32
+ ///
+ Float32 = 0x27,
+
+ ///
+ /// The int8
+ ///
+ Int8 = 0x03,
+
+ ///
+ /// The packed bit
+ ///
+ PackedBit = 0x10
+ }
+}
diff --git a/src/MongoDB.Bson/Serialization/Attributes/BsonVectorAttribute.cs b/src/MongoDB.Bson/Serialization/Attributes/BsonVectorAttribute.cs
new file mode 100644
index 00000000000..2bdb00b503e
--- /dev/null
+++ b/src/MongoDB.Bson/Serialization/Attributes/BsonVectorAttribute.cs
@@ -0,0 +1,53 @@
+/* Copyright 2010-present MongoDB Inc.
+*
+* Licensed 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.
+*/
+
+using System;
+using MongoDB.Bson.ObjectModel;
+using MongoDB.Bson.Serialization.Serializers;
+
+namespace MongoDB.Bson.Serialization.Attributes
+{
+ ///
+ /// Sets the representation for this field or property as BSON Vector and specifies the serialization options.
+ ///
+ [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
+ public sealed class BsonVectorAttribute : Attribute, IBsonMemberMapAttribute
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public BsonVectorAttribute(BsonVectorDataType dataType)
+ {
+ DataType = dataType;
+ }
+
+ ///
+ /// Gets the vector data type representation.
+ ///
+ public BsonVectorDataType DataType { get; init; }
+
+ ///
+ /// Applies the attribute to the member map.
+ ///
+ /// The member map.
+ public void Apply(BsonMemberMap memberMap)
+ {
+ var serializer = CreateSerializer(memberMap.MemberType);
+ memberMap.SetSerializer(serializer);
+ }
+
+ private IBsonSerializer CreateSerializer(Type type) => BsonVectorSerializer.CreateSerializer(type, DataType);
+ }
+}
diff --git a/src/MongoDB.Bson/Serialization/BsonBinaryDataExtensions.cs b/src/MongoDB.Bson/Serialization/BsonBinaryDataExtensions.cs
new file mode 100644
index 00000000000..cc458f8406e
--- /dev/null
+++ b/src/MongoDB.Bson/Serialization/BsonBinaryDataExtensions.cs
@@ -0,0 +1,69 @@
+/* Copyright 2010-present MongoDB Inc.
+*
+* Licensed 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.
+*/
+
+using System;
+using MongoDB.Bson.ObjectModel;
+
+namespace MongoDB.Bson.Serialization
+{
+ ///
+ /// Contains extensions methods for .
+ ///
+ public static class BsonBinaryDataExtensions
+ {
+ ///
+ /// Converts to .
+ ///
+ /// Data type of the Bson vector.
+ /// The binary data.
+ /// A instance.
+ public static BsonVectorBase ToBsonVector(this BsonBinaryData binaryData)
+ where TItem : struct
+ {
+ EnsureBsonVectorDataType(binaryData);
+
+ return BsonVectorReader.ReadBsonVector(binaryData.Bytes);
+ }
+
+ ///
+ /// Extracts Bson vector data from as bytes, padding and data type.
+ /// The result bytes should be interpreted according to the vector data type.
+ ///
+ /// The binary data.
+ /// Vector bytes, padding and datatype
+ public static (ReadOnlyMemory VectorDataBytes, byte Padding, BsonVectorDataType VectorDataType) ToBsonVectorAsBytes(this BsonBinaryData binaryData)
+ {
+ EnsureBsonVectorDataType(binaryData);
+
+ return BsonVectorReader.ReadBsonVectorAsBytes(binaryData.Bytes);
+ }
+
+ internal static (TItem[] Items, byte Padding, BsonVectorDataType vectorDataType) ToBsonVectorAsArray(this BsonBinaryData binaryData)
+ where TItem : struct
+ {
+ EnsureBsonVectorDataType(binaryData);
+
+ return BsonVectorReader.ReadBsonVectorAsArray(binaryData.Bytes);
+ }
+
+ private static void EnsureBsonVectorDataType(BsonBinaryData binaryData)
+ {
+ if (binaryData.SubType != BsonBinarySubType.Vector)
+ {
+ throw new InvalidOperationException($"Expected BsonBinary Vector subtype, but found {binaryData.SubType} instead.");
+ }
+ }
+ }
+}
diff --git a/src/MongoDB.Bson/Serialization/BsonObjectModelSerializationProvider.cs b/src/MongoDB.Bson/Serialization/BsonObjectModelSerializationProvider.cs
index 52521e461f8..0c94ba56982 100644
--- a/src/MongoDB.Bson/Serialization/BsonObjectModelSerializationProvider.cs
+++ b/src/MongoDB.Bson/Serialization/BsonObjectModelSerializationProvider.cs
@@ -52,7 +52,10 @@ static BsonObjectModelSerializationProvider()
{ typeof(BsonSymbol), BsonSymbolSerializer.Instance },
{ typeof(BsonTimestamp), BsonTimestampSerializer.Instance },
{ typeof(BsonUndefined), BsonUndefinedSerializer.Instance },
- { typeof(BsonValue), BsonValueSerializer.Instance }
+ { typeof(BsonValue), BsonValueSerializer.Instance },
+ { typeof(BsonVectorFloat32), BsonVectorSerializer.BsonVectorFloat32Serializer },
+ { typeof(BsonVectorInt8), BsonVectorSerializer.BsonVectorInt8Serializer },
+ { typeof(BsonVectorPackedBit), BsonVectorSerializer.BsonVectorPackedBitSerializer },
};
}
diff --git a/src/MongoDB.Bson/Serialization/BsonVectorExtensions.cs b/src/MongoDB.Bson/Serialization/BsonVectorExtensions.cs
new file mode 100644
index 00000000000..8ec75f987d8
--- /dev/null
+++ b/src/MongoDB.Bson/Serialization/BsonVectorExtensions.cs
@@ -0,0 +1,38 @@
+/* Copyright 2010-present MongoDB Inc.
+*
+* Licensed 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.
+*/
+
+namespace MongoDB.Bson.Serialization
+{
+ ///
+ /// Contains extensions methods for
+ ///
+ public static class BsonVectorExtensions
+ {
+ ///
+ /// Converts to .
+ ///
+ ///
+ /// The BSON vector.
+ /// A instance.
+ public static BsonBinaryData ToBsonBinaryData(this BsonVectorBase bsonVector)
+ where TItem : struct
+ {
+ var bytes = BsonVectorWriter.WriteToBytes(bsonVector);
+ var binaryData = new BsonBinaryData(bytes, BsonBinarySubType.Vector);
+
+ return binaryData;
+ }
+ }
+}
diff --git a/src/MongoDB.Bson/Serialization/BsonVectorReader.cs b/src/MongoDB.Bson/Serialization/BsonVectorReader.cs
new file mode 100644
index 00000000000..7dcb607a245
--- /dev/null
+++ b/src/MongoDB.Bson/Serialization/BsonVectorReader.cs
@@ -0,0 +1,131 @@
+/* Copyright 2010-present MongoDB Inc.
+*
+* Licensed 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.
+*/
+
+using System;
+using System.Runtime.InteropServices;
+using MongoDB.Bson.ObjectModel;
+
+namespace MongoDB.Bson.Serialization
+{
+ internal static class BsonVectorReader
+ {
+ public static BsonVectorBase ReadBsonVector(ReadOnlyMemory vectorData)
+ where TItem : struct
+ {
+ var (items, padding, vectorDataType) = ReadBsonVectorAsArray(vectorData);
+
+ return CreateBsonVector(items, padding, vectorDataType);
+ }
+
+ public static (TItem[] Items, byte Padding, BsonVectorDataType vectorDataType) ReadBsonVectorAsArray(ReadOnlyMemory vectorData)
+ where TItem : struct
+ {
+ var (vectorDataBytes, padding, vectorDataType) = ReadBsonVectorAsBytes(vectorData);
+ ValidateDataType(vectorDataType);
+
+ TItem[] elements;
+
+ switch (vectorDataType)
+ {
+ case BsonVectorDataType.Float32:
+ if (BitConverter.IsLittleEndian)
+ {
+ var singles = MemoryMarshal.Cast(vectorDataBytes.Span);
+ elements = (TItem[])(object)singles.ToArray();
+ }
+ else
+ {
+ throw new NotSupportedException("Bson Vector data is not supported on Big Endian architecture yet.");
+ }
+ break;
+ case BsonVectorDataType.Int8:
+ case BsonVectorDataType.PackedBit:
+ elements = (TItem[])(object)vectorDataBytes.ToArray();
+ break;
+ default:
+ throw new NotSupportedException($"Vector data type {vectorDataType} is not supported");
+ }
+
+ return (elements, padding, vectorDataType);
+ }
+
+ public static (ReadOnlyMemory VectorDataBytes, byte Padding, BsonVectorDataType VectorDataType) ReadBsonVectorAsBytes(ReadOnlyMemory vectorData)
+ {
+ if (vectorData.Length < 2)
+ {
+ throw new InvalidOperationException($"Invalid {nameof(vectorData)} size {vectorData.Length}");
+ }
+
+ var vectorDataSpan = vectorData.Span;
+ var vectorDataType = (BsonVectorDataType)vectorDataSpan[0];
+
+ var padding = vectorDataSpan[1];
+ if (padding > 7)
+ {
+ throw new InvalidOperationException($"Invalid padding size {padding}");
+ }
+
+ return (vectorData.Slice(2), padding, vectorDataType);
+ }
+
+ private static BsonVectorBase CreateBsonVector(TItem[] items, byte padding, BsonVectorDataType vectorDataType)
+ where TItem : struct
+ {
+ switch (vectorDataType)
+ {
+ case BsonVectorDataType.Float32:
+ {
+ return new BsonVectorFloat32(AsTypedArrayOrThrow()) as BsonVectorBase;
+ }
+ case BsonVectorDataType.Int8:
+ {
+ return new BsonVectorInt8(AsTypedArrayOrThrow()) as BsonVectorBase;
+ }
+ case BsonVectorDataType.PackedBit:
+ {
+ return new BsonVectorPackedBit(AsTypedArrayOrThrow(), padding) as BsonVectorBase;
+ }
+ default:
+ throw new NotSupportedException($"Vector data type {vectorDataType} is not supported");
+ }
+
+ TActualItem[] AsTypedArrayOrThrow()
+ {
+ if (items is not TActualItem[] result)
+ {
+ throw new InvalidOperationException($"Type {typeof(TItem)} is not supported with {vectorDataType} vector type.");
+ }
+
+ return result;
+ }
+ }
+
+ public static void ValidateDataType(BsonVectorDataType bsonVectorDataType)
+ {
+ var supportedType = bsonVectorDataType switch
+ {
+ BsonVectorDataType.Float32 => typeof(float),
+ BsonVectorDataType.Int8 => typeof(byte),
+ BsonVectorDataType.PackedBit => typeof(byte),
+ _ => throw new ArgumentException(nameof(bsonVectorDataType), "Unsupported vector datatype.")
+ };
+
+ if (supportedType != typeof(TItem))
+ {
+ throw new NotSupportedException($"Type {typeof(TItem)} is not supported with {bsonVectorDataType} vector type. Supported types are [{supportedType}].");
+ }
+ }
+ }
+}
diff --git a/src/MongoDB.Bson/Serialization/BsonVectorWriter.cs b/src/MongoDB.Bson/Serialization/BsonVectorWriter.cs
new file mode 100644
index 00000000000..e6c031df8f1
--- /dev/null
+++ b/src/MongoDB.Bson/Serialization/BsonVectorWriter.cs
@@ -0,0 +1,50 @@
+/* Copyright 2010-present MongoDB Inc.
+*
+* Licensed 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.
+*/
+
+using System;
+using System.Runtime.InteropServices;
+using MongoDB.Bson.ObjectModel;
+
+namespace MongoDB.Bson.Serialization
+{
+ internal static class BsonVectorWriter
+ {
+ public static byte[] WriteToBytes(BsonVectorBase bsonVector)
+ where TItem : struct
+ {
+ byte padding = 0;
+ if (bsonVector is BsonVectorPackedBit bsonVectorPackedBit)
+ {
+ padding = bsonVectorPackedBit.Padding;
+ }
+
+ return WriteToBytes(bsonVector.Data.Span, bsonVector.DataType, padding);
+ }
+
+ public static byte[] WriteToBytes(ReadOnlySpan vectorData, BsonVectorDataType bsonVectorDataType, byte padding)
+ where TItem : struct
+ {
+ if (!BitConverter.IsLittleEndian)
+ {
+ throw new NotSupportedException("Bson Vector data is not supported on Big Endian architecture yet.");
+ }
+
+ var vectorDataBytes = MemoryMarshal.Cast(vectorData);
+ byte[] result = [(byte)bsonVectorDataType, padding, .. vectorDataBytes];
+
+ return result;
+ }
+ }
+}
diff --git a/src/MongoDB.Bson/Serialization/Serializers/BsonVectorSerializer.cs b/src/MongoDB.Bson/Serialization/Serializers/BsonVectorSerializer.cs
new file mode 100644
index 00000000000..e8cc084213f
--- /dev/null
+++ b/src/MongoDB.Bson/Serialization/Serializers/BsonVectorSerializer.cs
@@ -0,0 +1,277 @@
+/* Copyright 2010-present MongoDB Inc.
+*
+* Licensed 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.
+*/
+
+using System;
+using MongoDB.Bson.IO;
+using MongoDB.Bson.ObjectModel;
+
+namespace MongoDB.Bson.Serialization.Serializers
+{
+ internal static class BsonVectorSerializer
+ {
+ public static BsonVectorSerializer BsonVectorFloat32Serializer { get; } = new BsonVectorSerializer(BsonVectorDataType.Float32);
+ public static BsonVectorSerializer BsonVectorInt8Serializer { get; } = new BsonVectorSerializer(BsonVectorDataType.Int8);
+ public static BsonVectorSerializer BsonVectorPackedBitSerializer { get; } = new BsonVectorSerializer(BsonVectorDataType.PackedBit);
+
+ public static IBsonSerializer CreateArraySerializer(Type itemType, BsonVectorDataType bsonVectorDataType) =>
+ CreateSerializerInstance(typeof(ArrayAsBsonVectorSerializer<>).MakeGenericType(itemType), bsonVectorDataType);
+
+ public static IBsonSerializer CreateBsonVectorSerializer(Type bsonVectorType, Type itemType, BsonVectorDataType bsonVectorDataType) =>
+ CreateSerializerInstance(typeof(BsonVectorSerializer<,>).MakeGenericType(bsonVectorType, itemType), bsonVectorDataType);
+
+ public static IBsonSerializer CreateMemorySerializer(Type itemType, BsonVectorDataType bsonVectorDataType) =>
+ CreateSerializerInstance(typeof(MemoryAsBsonVectorSerializer<>).MakeGenericType(itemType), bsonVectorDataType);
+
+ public static IBsonSerializer CreateReadonlyMemorySerializer(Type itemType, BsonVectorDataType bsonVectorDataType) =>
+ CreateSerializerInstance(typeof(ReadOnlyMemoryAsBsonVectorSerializer<>).MakeGenericType(itemType), bsonVectorDataType);
+
+ public static IBsonSerializer CreateSerializer(Type type, BsonVectorDataType bsonVectorDataType)
+ {
+ // Arrays
+ if (type.IsArray)
+ {
+ var itemType = type.GetElementType();
+ return CreateArraySerializer(itemType, bsonVectorDataType);
+ }
+
+ // BsonVector
+ if (type == typeof(BsonVectorFloat32) ||
+ type == typeof(BsonVectorInt8) ||
+ type == typeof(BsonVectorPackedBit))
+ {
+ return CreateBsonVectorSerializer(type, GetItemType(type.BaseType), bsonVectorDataType);
+ }
+
+ // Memory/ReadonlyMemory
+ var genericTypeDefinition = type.IsGenericType ? type.GetGenericTypeDefinition() : null;
+ if (genericTypeDefinition == typeof(Memory<>))
+ {
+ return CreateMemorySerializer(GetItemType(type), bsonVectorDataType);
+ }
+ else if (genericTypeDefinition == typeof(ReadOnlyMemory<>))
+ {
+ return CreateReadonlyMemorySerializer(GetItemType(type), bsonVectorDataType);
+ }
+
+ throw new NotSupportedException($"Type {type} is not supported for a binary vector.");
+
+ Type GetItemType(Type collectionType)
+ {
+ var genericArguments = collectionType.GetGenericArguments();
+ if (genericArguments.Length != 1)
+ {
+ throw new NotSupportedException($"Type {type} is not supported for a binary vector.");
+ }
+
+ return genericArguments[0];
+ }
+ }
+
+ private static IBsonSerializer CreateSerializerInstance(Type vectorSerializerType, BsonVectorDataType bsonVectorDataType) =>
+ (IBsonSerializer)Activator.CreateInstance(vectorSerializerType, bsonVectorDataType);
+ }
+
+ ///
+ /// Represents a serializer for TItemContainer values represented as a BsonVector.
+ ///
+ /// The items container, for example or .
+ /// The .NET data type.
+ internal abstract class BsonVectorSerializerBase : SerializerBase
+ where TItem : struct
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Type of the bson vector data.
+ protected BsonVectorSerializerBase(BsonVectorDataType bsonVectorDataType)
+ {
+ BsonVectorReader.ValidateDataType(bsonVectorDataType);
+
+ VectorDataType = bsonVectorDataType;
+ }
+
+ ///
+ /// Gets the type of the vector data.
+ ///
+ public BsonVectorDataType VectorDataType { get; }
+
+ ///
+ public override int GetHashCode() => 0;
+
+ ///
+ public override bool Equals(object obj)
+ {
+ if (object.ReferenceEquals(obj, null)) { return false; }
+ if (object.ReferenceEquals(this, obj)) { return true; }
+ return
+ base.Equals(obj) &&
+ obj is BsonVectorSerializerBase other &&
+ object.Equals(VectorDataType, other.VectorDataType);
+ }
+
+ protected BsonBinaryData ReadAndValidateBsonBinaryData(IBsonReader bsonReader)
+ {
+ var bsonType = bsonReader.GetCurrentBsonType();
+ if (bsonType != BsonType.Binary)
+ {
+ throw CreateCannotDeserializeFromBsonTypeException(bsonType);
+ }
+
+ var binaryData = bsonReader.ReadBinaryData();
+
+ return binaryData;
+ }
+ }
+
+ ///
+ /// Represents a serializer for .
+ ///
+ /// The concrete type derived from .
+ /// The .NET data type.
+ internal sealed class BsonVectorSerializer : BsonVectorSerializerBase
+ where TItemContainer : BsonVectorBase
+ where TItem : struct
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public BsonVectorSerializer(BsonVectorDataType bsonVectorDataType) :
+ base(bsonVectorDataType)
+ {
+ }
+
+ ///
+ public override sealed TItemContainer Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
+ {
+ var binaryData = ReadAndValidateBsonBinaryData(context.Reader);
+ return (TItemContainer)binaryData.ToBsonVector();
+ }
+
+ ///
+ public override sealed void Serialize(BsonSerializationContext context, BsonSerializationArgs args, TItemContainer value)
+ {
+ var binaryData = value.ToBsonBinaryData();
+
+ context.Writer.WriteBinaryData(binaryData);
+ }
+ }
+
+ ///
+ /// A base class for serializers for containers represented as a BsonVector.
+ ///
+ /// The collection type.
+ /// The .NET data type.
+ internal abstract class ItemContainerAsBsonVectorSerializer : BsonVectorSerializerBase
+ where TItem : struct
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ protected ItemContainerAsBsonVectorSerializer(BsonVectorDataType bsonVectorDataType) :
+ base(bsonVectorDataType)
+ {
+ }
+
+ ///
+ public sealed override TItemContainer Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
+ {
+ var binaryData = ReadAndValidateBsonBinaryData(context.Reader);
+ var (elements, padding, _) = binaryData.ToBsonVectorAsArray();
+
+ if (padding != 0)
+ {
+ throw new FormatException($"Padding is supported only in {nameof(BsonVectorPackedBit)} data type.");
+ }
+
+ return CreateResult(elements);
+ }
+
+ ///
+ public sealed override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, TItemContainer value)
+ {
+ var vectorData = GetItemsContainerSpan(value);
+ var bytes = BsonVectorWriter.WriteToBytes(vectorData, VectorDataType, 0);
+ var binaryData = new BsonBinaryData(bytes, BsonBinarySubType.Vector);
+
+ context.Writer.WriteBinaryData(binaryData);
+ }
+
+ private protected abstract TItemContainer CreateResult(TItem[] elements);
+ private protected abstract ReadOnlySpan GetItemsContainerSpan(TItemContainer data);
+ }
+
+ ///
+ /// Represents a serializer for BSON vector to/from array of .
+ ///
+ /// The .NET data type.
+ internal sealed class ArrayAsBsonVectorSerializer : ItemContainerAsBsonVectorSerializer
+ where TItem : struct
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ArrayAsBsonVectorSerializer(BsonVectorDataType bsonVectorDataType) : base(bsonVectorDataType)
+ {
+ }
+
+ private protected override ReadOnlySpan GetItemsContainerSpan(TItem[] data) =>
+ data;
+
+ private protected override TItem[] CreateResult(TItem[] elements) =>
+ elements;
+ }
+
+ ///
+ /// Represents a serializer for BSON vector to/from
+ ///
+ /// The .NET data type.
+ internal sealed class MemoryAsBsonVectorSerializer : ItemContainerAsBsonVectorSerializer, TItem>
+ where TItem : struct
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MemoryAsBsonVectorSerializer(BsonVectorDataType bsonVectorDataType) : base(bsonVectorDataType)
+ {
+ }
+
+ private protected override ReadOnlySpan GetItemsContainerSpan(Memory data) =>
+ data.Span;
+
+ private protected override Memory CreateResult(TItem[] elements) =>
+ new(elements);
+ }
+
+ ///
+ /// Represents a serializer for .
+ ///
+ /// The .NET data type.
+ internal sealed class ReadOnlyMemoryAsBsonVectorSerializer : ItemContainerAsBsonVectorSerializer, TItem>
+ where TItem : struct
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ReadOnlyMemoryAsBsonVectorSerializer(BsonVectorDataType bsonVectorDataType) : base(bsonVectorDataType)
+ {
+ }
+
+ private protected override ReadOnlySpan GetItemsContainerSpan(ReadOnlyMemory data) =>
+ data.Span;
+
+ private protected override ReadOnlyMemory CreateResult(TItem[] elements) =>
+ new(elements);
+ }
+}
diff --git a/src/MongoDB.Driver/BsonVectorExtensions.cs b/src/MongoDB.Driver/BsonVectorExtensions.cs
new file mode 100644
index 00000000000..1be3f64ab18
--- /dev/null
+++ b/src/MongoDB.Driver/BsonVectorExtensions.cs
@@ -0,0 +1,43 @@
+/* Copyright 2010-present MongoDB Inc.
+*
+* Licensed 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.
+*/
+
+using System;
+using MongoDB.Bson;
+using MongoDB.Bson.Serialization;
+
+namespace MongoDB.Driver
+{
+ ///
+ /// Contains extensions methods for
+ ///
+ public static class BsonVectorDriverExtensions
+ {
+ ///
+ /// Converts to .
+ ///
+ ///
+ /// The BSON vector.
+ /// A instance.
+ public static QueryVector ToQueryVector(this BsonVectorBase bsonVector)
+ where T : struct =>
+ bsonVector switch
+ {
+ BsonVectorFloat32 bsonVectorFloat32 => new(bsonVectorFloat32.ToBsonBinaryData()),
+ BsonVectorInt8 bsonVectorInt8 => new(bsonVectorInt8.ToBsonBinaryData()),
+ BsonVectorPackedBit bsonVectorPackedBit => new(bsonVectorPackedBit.ToBsonBinaryData()),
+ _ => throw new InvalidOperationException($"Invalidate Bson Vector type {bsonVector?.GetType()}")
+ };
+ }
+}
diff --git a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs
index 90c3c1f84e7..53f6017f6e8 100644
--- a/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs
+++ b/src/MongoDB.Driver/PipelineStageDefinitionBuilder.cs
@@ -1996,7 +1996,7 @@ public static PipelineStageDefinition VectorSearch(
ClientSideProjectionHelper.ThrowIfClientSideProjection(args.DocumentSerializer, operatorName);
var vectorSearchOperator = new BsonDocument
{
- { "queryVector", queryVector.Array },
+ { "queryVector", queryVector.Vector },
{ "path", field.Render(args).FieldName },
{ "limit", limit },
{ "numCandidates", options?.NumberOfCandidates ?? limit * 10, options?.Exact != true },
diff --git a/src/MongoDB.Driver/QueryVector.cs b/src/MongoDB.Driver/QueryVector.cs
index 2af2615826a..e0faeb54d9d 100644
--- a/src/MongoDB.Driver/QueryVector.cs
+++ b/src/MongoDB.Driver/QueryVector.cs
@@ -29,14 +29,27 @@ namespace MongoDB.Driver
public sealed class QueryVector
{
///
- /// Gets the underlying BSON array.
+ /// Gets the underlying bson value representing the vector.
+ /// Possible values are or encoding
///
- internal BsonArray Array { get; } // do not make public because in the case of ReadOnlyMemory the BsonArray subclass is not fully functional
+ internal BsonValue Vector { get; } // do not make public because in the case of ReadOnlyMemory the BsonArray subclass is not fully functional
private QueryVector(BsonArray array)
{
Ensure.IsNotNullOrEmpty(array, nameof(array));
- Array = array;
+ Vector = array;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The bson binary data.
+ public QueryVector(BsonBinaryData bsonBinaryData)
+ {
+ Ensure.IsNotNull(bsonBinaryData, nameof(bsonBinaryData));
+ Ensure.IsEqualTo(bsonBinaryData.SubType, BsonBinarySubType.Vector, nameof(bsonBinaryData.SubType));
+
+ Vector = bsonBinaryData;
}
///
@@ -94,7 +107,7 @@ public QueryVector(ReadOnlyMemory readOnlyMemory) :
public static implicit operator QueryVector(float[] array) => new(array);
///
- /// Performs an implicit conversion from a of to .
+ /// Performs an implicit conversion from a of to .
///
/// The readOnlyMemory.
///
@@ -112,13 +125,40 @@ public QueryVector(ReadOnlyMemory readOnlyMemory) :
public static implicit operator QueryVector(int[] array) => new(array);
///
- /// Performs an implicit conversion from a of to .
+ /// Performs an implicit conversion from a of to .
///
/// The readOnlyMemory.
///
/// The result of the conversion.
///
public static implicit operator QueryVector(ReadOnlyMemory readOnlyMemory) => new(readOnlyMemory);
+
+ ///
+ /// Performs an implicit conversion from a of to .
+ ///
+ /// The bson vector int8.
+ ///
+ /// The result of the conversion.
+ ///
+ public static implicit operator QueryVector(BsonVectorInt8 bsonVectorInt8) => bsonVectorInt8.ToQueryVector();
+
+ ///
+ /// Performs an implicit conversion from a of to .
+ ///
+ /// The bson vector float32.
+ ///
+ /// The result of the conversion.
+ ///
+ public static implicit operator QueryVector(BsonVectorFloat32 bsonVectorFloat32) => bsonVectorFloat32.ToQueryVector();
+
+ ///
+ /// Performs an implicit conversion from a of to .
+ ///
+ /// The bson vector packed bit.
+ ///
+ /// The result of the conversion.
+ ///
+ public static implicit operator QueryVector(BsonVectorPackedBit bsonVectorPackedBit) => bsonVectorPackedBit.ToQueryVector();
}
// WARNING: this class is a very partial implementation of a BsonArray subclass
diff --git a/tests/MongoDB.Bson.Tests/Serialization/Attributes/BsonVectorAttributeTests.cs b/tests/MongoDB.Bson.Tests/Serialization/Attributes/BsonVectorAttributeTests.cs
new file mode 100644
index 00000000000..5f965f9c6a2
--- /dev/null
+++ b/tests/MongoDB.Bson.Tests/Serialization/Attributes/BsonVectorAttributeTests.cs
@@ -0,0 +1,171 @@
+/* Copyright 2010-present MongoDB Inc.
+*
+* Licensed 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.
+*/
+
+using System;
+using System.Collections.Generic;
+using FluentAssertions;
+using MongoDB.Bson.ObjectModel;
+using MongoDB.Bson.Serialization;
+using MongoDB.Bson.Serialization.Attributes;
+using MongoDB.Bson.Serialization.Serializers;
+using Xunit;
+
+namespace MongoDB.Bson.Tests.Serialization.Attributes
+{
+ public class BsonVectorAttributeTests
+ {
+ [Fact]
+ public void BsonVectorAttribute_should_set_bsonVectorArraySerializer_for_array()
+ {
+ var classMap = BsonClassMap.LookupClassMap(typeof(ArrayHolder));
+
+ AssertSerializer, byte[], byte>(classMap, nameof(ArrayHolder.ValuesByte), BsonVectorDataType.Int8);
+ AssertSerializer, byte[], byte>(classMap, nameof(ArrayHolder.ValuesPackedBit), BsonVectorDataType.PackedBit);
+ AssertSerializer, float[], float>(classMap, nameof(ArrayHolder.ValuesFloat), BsonVectorDataType.Float32);
+ }
+
+ [Fact]
+ public void BsonVectorAttribute_should_set_bsonVectorSerializer_for_bsonVector()
+ {
+ var classMap = BsonClassMap.LookupClassMap(typeof(BsonVectorHolder));
+
+ AssertSerializer, BsonVectorInt8, byte>(classMap, nameof(BsonVectorHolder.ValuesInt8), BsonVectorDataType.Int8);
+ AssertSerializer, BsonVectorPackedBit, byte>(classMap, nameof(BsonVectorHolder.ValuesPackedBit), BsonVectorDataType.PackedBit);
+ AssertSerializer, BsonVectorFloat32, float>(classMap, nameof(BsonVectorHolder.ValuesFloat), BsonVectorDataType.Float32);
+ }
+
+ [Fact]
+ public void BsonVectorAttribute_should_set_bsonVectorMemorySerializer_for_memory()
+ {
+ var classMap = BsonClassMap.LookupClassMap(typeof(MemoryHolder));
+
+ AssertSerializer, Memory, byte>(classMap, nameof(MemoryHolder.ValuesByte), BsonVectorDataType.Int8);
+ AssertSerializer, Memory, byte>(classMap, nameof(MemoryHolder.ValuesPackedBit), BsonVectorDataType.PackedBit);
+ AssertSerializer, Memory, float>(classMap, nameof(MemoryHolder.ValuesFloat), BsonVectorDataType.Float32);
+ }
+
+ [Fact]
+ public void BsonVectorAttribute_should_set_bsonVectorReadonlyMemorySerializer_for_readOnlyMemory()
+ {
+ var classMap = BsonClassMap.LookupClassMap(typeof(ReadOnlyMemoryHolder));
+
+ AssertSerializer, ReadOnlyMemory, byte>(classMap, nameof(ReadOnlyMemoryHolder.ValuesByte), BsonVectorDataType.Int8);
+ AssertSerializer, ReadOnlyMemory, byte>(classMap, nameof(ReadOnlyMemoryHolder.ValuesPackedBit), BsonVectorDataType.PackedBit);
+ AssertSerializer, ReadOnlyMemory, float>(classMap, nameof(ReadOnlyMemoryHolder.ValuesFloat), BsonVectorDataType.Float32);
+ }
+
+ [Theory]
+ [InlineData(nameof(InvalidTypesHolder.List))]
+ [InlineData(nameof(InvalidTypesHolder.Dictionary))]
+ public void BsonVectorAttribute_should_throw_on_invalid_type(string memberName)
+ {
+ var memberInfo = typeof(InvalidTypesHolder).GetProperty(memberName);
+ var classMap = new BsonClassMap(cm => cm.MapMember(memberInfo));
+
+ var exception = Record.Exception(() => classMap.AutoMap());
+
+ exception.Should().BeOfType();
+ exception.Message.Should().Be($"Type {memberInfo.PropertyType} is not supported for a binary vector.");
+ }
+
+ [Fact]
+ public void BsonVectorSerializer_should_be_used_for_bsonvector_without_attribute()
+ {
+ var classMap = BsonClassMap.LookupClassMap(typeof(BsonVectorNoAttributeHolder));
+
+ AssertSerializer, BsonVectorInt8, byte>(classMap, nameof(BsonVectorNoAttributeHolder.ValuesInt8), BsonVectorDataType.Int8);
+ AssertSerializer, BsonVectorPackedBit, byte>(classMap, nameof(BsonVectorNoAttributeHolder.ValuesPackedBit), BsonVectorDataType.PackedBit);
+ AssertSerializer, BsonVectorFloat32, float>(classMap, nameof(BsonVectorNoAttributeHolder.ValuesFloat), BsonVectorDataType.Float32);
+ }
+
+ private void AssertSerializer(BsonClassMap classMap, string memberName, BsonVectorDataType bsonVectorDataType)
+ where TSerializer : BsonVectorSerializerBase
+ where TITem : struct
+ {
+ var memberMap = classMap.GetMemberMap(memberName);
+ var serializer = memberMap.GetSerializer();
+
+ var vectorSerializer = serializer.Should().BeOfType().Subject;
+
+ vectorSerializer.VectorDataType.Should().Be(bsonVectorDataType);
+ }
+
+ public class ArrayHolder
+ {
+ [BsonVector(BsonVectorDataType.Int8)]
+ public byte[] ValuesByte { get; set; }
+
+ [BsonVector(BsonVectorDataType.PackedBit)]
+ public byte[] ValuesPackedBit { get; set; }
+
+ [BsonVector(BsonVectorDataType.Float32)]
+ public float[] ValuesFloat { get; set; }
+ }
+
+ public class BsonVectorHolder
+ {
+ [BsonVector(BsonVectorDataType.Int8)]
+ public BsonVectorInt8 ValuesInt8 { get; set; }
+
+ [BsonVector(BsonVectorDataType.PackedBit)]
+ public BsonVectorPackedBit ValuesPackedBit { get; set; }
+
+ [BsonVector(BsonVectorDataType.Float32)]
+ public BsonVectorFloat32 ValuesFloat { get; set; }
+ }
+
+ public class BsonVectorNoAttributeHolder
+ {
+ public BsonVectorInt8 ValuesInt8 { get; set; }
+
+ public BsonVectorPackedBit ValuesPackedBit { get; set; }
+
+ public BsonVectorFloat32 ValuesFloat { get; set; }
+ }
+
+ public class MemoryHolder
+ {
+ [BsonVector(BsonVectorDataType.Int8)]
+ public Memory ValuesByte { get; set; }
+
+ [BsonVector(BsonVectorDataType.PackedBit)]
+ public Memory ValuesPackedBit { get; set; }
+
+ [BsonVector(BsonVectorDataType.Float32)]
+ public Memory ValuesFloat { get; set; }
+ }
+
+ public class ReadOnlyMemoryHolder
+ {
+ [BsonVector(BsonVectorDataType.Int8)]
+ public ReadOnlyMemory ValuesByte { get; set; }
+
+ [BsonVector(BsonVectorDataType.PackedBit)]
+ public ReadOnlyMemory ValuesPackedBit { get; set; }
+
+ [BsonVector(BsonVectorDataType.Float32)]
+ public ReadOnlyMemory ValuesFloat { get; set; }
+ }
+
+ public class InvalidTypesHolder
+ {
+ [BsonVector(BsonVectorDataType.Int8)]
+ public List List { get; set; }
+
+ [BsonVector(BsonVectorDataType.Int8)]
+ public Dictionary Dictionary { get; set; }
+ }
+ }
+}
diff --git a/tests/MongoDB.Bson.Tests/Serialization/Serializers/BsonVectorSerializerTests.cs b/tests/MongoDB.Bson.Tests/Serialization/Serializers/BsonVectorSerializerTests.cs
new file mode 100644
index 00000000000..f528d8bb358
--- /dev/null
+++ b/tests/MongoDB.Bson.Tests/Serialization/Serializers/BsonVectorSerializerTests.cs
@@ -0,0 +1,424 @@
+/* Copyright 2010-present MongoDB Inc.
+*
+* Licensed 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.
+*/
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using FluentAssertions;
+using MongoDB.Bson.IO;
+using MongoDB.Bson.ObjectModel;
+using MongoDB.Bson.Serialization;
+using MongoDB.Bson.Serialization.Serializers;
+using Xunit;
+
+namespace MongoDB.Bson.Tests.Serialization
+{
+ public class BsonVectorSerializerTests
+ {
+ [Theory]
+ [MemberData(nameof(TestData))]
+ public void ArrayAsBsonVectorSerializer_should_deserialize_bson_vector(BsonVectorDataType dataType, int elementCount, T _)
+ where T : struct
+ {
+ var subject = new ArrayAsBsonVectorSerializer(dataType);
+
+ var (vector, vectorBson) = GetTestData(dataType, elementCount, 0);
+ var expectedArray = vector.Data.ToArray();
+
+ var actualArray = DeserializeFromBinaryData(vectorBson, subject);
+
+ actualArray.ShouldAllBeEquivalentTo(expectedArray);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestData))]
+ public void ArrayAsBsonVectorSerializer_should_serialize_bson_vector(BsonVectorDataType dataType, int elementCount, T _)
+ where T : struct
+ {
+ var subject = new ArrayAsBsonVectorSerializer(dataType);
+
+ var (vector, expectedBson) = GetTestData(dataType, elementCount, 0);
+ var array = vector.Data.ToArray();
+
+ var binaryData = SerializeToBinaryData(array, subject);
+
+ Assert.Equal(BsonBinarySubType.Vector, binaryData.SubType);
+ Assert.Equal(expectedBson, binaryData.Bytes);
+ }
+
+ [Fact]
+ public void ArrayAsBsonVectorSerializer_should_throw_on_non_zero_padding()
+ {
+ var subject = new ArrayAsBsonVectorSerializer(BsonVectorDataType.PackedBit);
+
+ var (vector, vectorBson) = GetTestData(BsonVectorDataType.PackedBit, 2, 1);
+ var expectedArray = vector.Data.ToArray();
+
+ var exception = Record.Exception(() => DeserializeFromBinaryData(vectorBson, subject));
+ exception.Should().BeOfType();
+ exception.Message.Should().Contain("Padding");
+ }
+
+ [Theory]
+ [MemberData(nameof(TestDataBsonVector))]
+ public void BsonVectorSerializer_should_serialize_bson_vector(BsonVectorDataType dataType, int elementsCount, int bitsPadding, T _)
+ where T : struct
+ {
+ var subject = CreateBsonVectorSerializer(dataType);
+
+ var (vector, expectedBson) = GetTestData(dataType, elementsCount, (byte)bitsPadding);
+
+ var binaryData = SerializeToBinaryData(vector, subject);
+
+ Assert.Equal(BsonBinarySubType.Vector, binaryData.SubType);
+ Assert.Equal(expectedBson, binaryData.Bytes);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestDataBsonVector))]
+ public void BsonVectorSerializer_should_deserialize_bson_vector(BsonVectorDataType dataType, int elementsCount, int bitsPadding, T _)
+ where T : struct
+ {
+ var subject = CreateBsonVectorSerializer(dataType);
+
+ var (vector, vectorBson) = GetTestData(dataType, elementsCount, (byte)bitsPadding);
+ var expectedArray = vector.Data.ToArray();
+ var expectedType = (dataType) switch
+ {
+ BsonVectorDataType.Float32 => typeof(BsonVectorFloat32),
+ BsonVectorDataType.PackedBit => typeof(BsonVectorPackedBit),
+ BsonVectorDataType.Int8 => typeof(BsonVectorInt8),
+ _ => throw new ArgumentOutOfRangeException(nameof(dataType))
+ };
+
+ var bsonVector = DeserializeFromBinaryData>(vectorBson, subject);
+
+ bsonVector.Should().BeOfType(expectedType);
+ bsonVector.Data.ToArray().ShouldBeEquivalentTo(expectedArray);
+
+ if (bsonVector is BsonVectorPackedBit vectorPackedBit)
+ {
+ vectorPackedBit.Padding.Should().Be((byte)bitsPadding);
+ }
+ }
+
+
+ [Theory]
+ [InlineData(BsonVectorDataType.Int8, typeof(BsonVectorSerializer))]
+ [InlineData(BsonVectorDataType.Int8, typeof(ArrayAsBsonVectorSerializer))]
+ [InlineData(BsonVectorDataType.Int8, typeof(ReadOnlyMemoryAsBsonVectorSerializer))]
+ [InlineData(BsonVectorDataType.Int8, typeof(MemoryAsBsonVectorSerializer))]
+ [InlineData(BsonVectorDataType.PackedBit, typeof(BsonVectorSerializer))]
+ [InlineData(BsonVectorDataType.PackedBit, typeof(ArrayAsBsonVectorSerializer))]
+ [InlineData(BsonVectorDataType.PackedBit, typeof(ReadOnlyMemoryAsBsonVectorSerializer))]
+ [InlineData(BsonVectorDataType.PackedBit, typeof(MemoryAsBsonVectorSerializer))]
+ [InlineData(BsonVectorDataType.Float32, typeof(BsonVectorSerializer))]
+ [InlineData(BsonVectorDataType.Float32, typeof(ArrayAsBsonVectorSerializer))]
+ [InlineData(BsonVectorDataType.Float32, typeof(ReadOnlyMemoryAsBsonVectorSerializer))]
+ [InlineData(BsonVectorDataType.Float32, typeof(MemoryAsBsonVectorSerializer))]
+ public void BsonVectorSerializer_should_throw_on_datatype_and_itemtype_mismatch(BsonVectorDataType dataType, Type serializerType)
+ {
+ var itemType = serializerType.BaseType.GetGenericArguments().ElementAt(1);
+
+ var exception = Record.Exception(() => Activator.CreateInstance(serializerType, dataType)).InnerException;
+ exception.Should().BeOfType();
+
+ exception.Message.Should().Contain(itemType.ToString());
+ exception.Message.Should().Contain(dataType.ToString());
+ }
+
+ [Fact]
+ public void BsonVectorSerializer_should_roundtrip_bsonvector_without_attribute()
+ {
+ var bsonVectorHolder = new BsonVectorNoAttributeHolder()
+ {
+ ValuesFloat = new BsonVectorFloat32(new float[] { 1.1f, 2.2f, 3.3f }),
+ ValuesInt8 = new BsonVectorInt8(new byte[] { 1, 2, 3 }),
+ ValuesPackedBit = new BsonVectorPackedBit(new byte[] { 1, 2, 3 }, 0)
+ };
+
+ var bson = bsonVectorHolder.ToBson();
+
+ var bsonVectorHolderDehydrated = BsonSerializer.Deserialize(bson);
+
+ bsonVectorHolderDehydrated.ValuesFloat.Data.ToArray().ShouldBeEquivalentTo(bsonVectorHolder.ValuesFloat.Data.ToArray());
+ bsonVectorHolderDehydrated.ValuesInt8.Data.ToArray().ShouldBeEquivalentTo(bsonVectorHolder.ValuesInt8.Data.ToArray());
+ bsonVectorHolderDehydrated.ValuesPackedBit.Data.ToArray().ShouldBeEquivalentTo(bsonVectorHolder.ValuesPackedBit.Data.ToArray());
+ }
+
+ [Theory]
+ [MemberData(nameof(TestData))]
+ public void MemoryAsBsonVectorSerializer_should_serialize_bson_vector(BsonVectorDataType dataType, int elementCount, T _)
+ where T : struct
+ {
+ var subject = new MemoryAsBsonVectorSerializer(dataType);
+
+ var (vector, expectedBson) = GetTestData(dataType, elementCount, 0);
+ var memory = new Memory(vector.Data.ToArray());
+
+ var binaryData = SerializeToBinaryData(memory, subject);
+
+ Assert.Equal(BsonBinarySubType.Vector, binaryData.SubType);
+ Assert.Equal(expectedBson, binaryData.Bytes);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestData))]
+ public void MemoryAsBsonVectorSerializer_should_deserialize_bson_vector(BsonVectorDataType dataType, int elementCount, T _)
+ where T : struct
+ {
+ var subject = new MemoryAsBsonVectorSerializer(dataType);
+
+ var (vector, vectorBson) = GetTestData(dataType, elementCount, 0);
+ var expectedArray = vector.Data.ToArray();
+
+ var actualMemory = DeserializeFromBinaryData>(vectorBson, subject);
+
+ actualMemory.ToArray().ShouldBeEquivalentTo(expectedArray);
+ }
+
+ [Fact]
+ public void MemoryAsBsonVectorSerializer_should_throw_on_non_zero_padding()
+ {
+ var subject = new ReadOnlyMemoryAsBsonVectorSerializer(BsonVectorDataType.PackedBit);
+
+ var (vector, vectorBson) = GetTestData(BsonVectorDataType.PackedBit, 2, 1);
+ var expectedArray = vector.Data.ToArray();
+
+ var exception = Record.Exception(() => DeserializeFromBinaryData>(vectorBson, subject));
+ exception.Should().BeOfType();
+ exception.Message.Should().Contain("Padding");
+ }
+
+ [Theory]
+ [MemberData(nameof(TestData))]
+ public void ReadonlyMemoryAsBsonVectorSerializer_should_serialize_bson_vector(BsonVectorDataType dataType, int elementCount, T _)
+ where T : struct
+ {
+ var subject = new ReadOnlyMemoryAsBsonVectorSerializer(dataType);
+
+ var (vector, expectedBson) = GetTestData(dataType, elementCount, 0);
+
+ var binaryData = SerializeToBinaryData(vector.Data, subject);
+
+ Assert.Equal(BsonBinarySubType.Vector, binaryData.SubType);
+ Assert.Equal(expectedBson, binaryData.Bytes);
+ }
+
+ [Theory]
+ [MemberData(nameof(TestData))]
+ public void ReadonlyMemoryAsBsonVectorSerializer_should_deserialize_bson_vector(BsonVectorDataType dataType, int elementCount, T _)
+ where T : struct
+ {
+ var subject = new ReadOnlyMemoryAsBsonVectorSerializer(dataType);
+
+ var (vector, vectorBson) = GetTestData(dataType, elementCount, 0);
+ var expectedArray = vector.Data.ToArray();
+
+ var readonlyMemory = DeserializeFromBinaryData>(vectorBson, subject);
+
+ readonlyMemory.ToArray().ShouldBeEquivalentTo(expectedArray);
+ }
+
+ [Fact]
+ public void ReadonlyMemoryAsBsonVectorSerializer_should_throw_on_non_zero_padding()
+ {
+ var subject = new ReadOnlyMemoryAsBsonVectorSerializer(BsonVectorDataType.PackedBit);
+
+ var (vector, vectorBson) = GetTestData(BsonVectorDataType.PackedBit, 2, 1);
+ var expectedArray = vector.Data.ToArray();
+
+ var exception = Record.Exception(() => DeserializeFromBinaryData>(vectorBson, subject));
+ exception.Should().BeOfType();
+ exception.Message.Should().Contain("Padding");
+ }
+
+ [Fact]
+ public void Equals_null_should_return_false()
+ {
+ var x = new ArrayAsBsonVectorSerializer(BsonVectorDataType.Float32);
+
+ var result = x.Equals(null);
+
+ result.Should().Be(false);
+ }
+
+ [Fact]
+ public void Equals_object_should_return_false()
+ {
+ var x = new ArrayAsBsonVectorSerializer(BsonVectorDataType.Float32);
+ var y = new object();
+
+ var result = x.Equals(y);
+
+ result.Should().Be(false);
+ }
+
+ [Fact]
+ public void Equals_self_should_return_true()
+ {
+ var x = new ArrayAsBsonVectorSerializer(BsonVectorDataType.Float32);
+
+ var result = x.Equals(x);
+
+ result.Should().Be(true);
+ }
+
+ [Fact]
+ public void Equals_with_equal_fields_should_return_true()
+ {
+ var x = new ArrayAsBsonVectorSerializer(BsonVectorDataType.Float32);
+ var y = new ArrayAsBsonVectorSerializer(BsonVectorDataType.Float32);
+
+ var result = x.Equals(y);
+
+ result.Should().Be(true);
+ }
+
+ [Fact]
+ public void Equals_with_not_equal_field_should_return_false()
+ {
+ var x = new ArrayAsBsonVectorSerializer(BsonVectorDataType.Float32);
+ var y = new ArrayAsBsonVectorSerializer(BsonVectorDataType.PackedBit);
+
+ var result = x.Equals(y);
+
+ result.Should().Be(false);
+ }
+
+ [Fact]
+ public void GetHashCode_should_return_zero()
+ {
+ var x = new ArrayAsBsonVectorSerializer(BsonVectorDataType.Float32);
+
+ var result = x.GetHashCode();
+
+ result.Should().Be(0);
+ }
+
+ private TCollection DeserializeFromBinaryData(byte[] vectorBson, IBsonSerializer serializer)
+ {
+ var binaryData = new BsonBinaryData(vectorBson, BsonBinarySubType.Vector);
+ var bsonDocument = new BsonDocument("vector", binaryData);
+ var bson = bsonDocument.ToBson();
+
+ using var memoryStream = new MemoryStream(bson);
+ using var reader = new BsonBinaryReader(memoryStream, new());
+ var context = BsonDeserializationContext.CreateRoot(reader);
+
+ reader.ReadStartDocument();
+ reader.ReadName("vector");
+
+ var result = serializer.Deserialize(context, new());
+
+ reader.ReadEndDocument();
+
+ return (TCollection)result;
+ }
+
+ private BsonBinaryData SerializeToBinaryData(TCollection collection, IBsonSerializer serializer)
+ {
+ using var memoryStream = new MemoryStream();
+ using var writer = new BsonBinaryWriter(memoryStream, new());
+ var context = BsonSerializationContext.CreateRoot(writer);
+
+ writer.WriteStartDocument();
+ writer.WriteName("vector");
+
+ serializer.Serialize(context, new(), collection);
+
+ writer.WriteEndDocument();
+
+ var document = BsonSerializer.Deserialize(memoryStream.ToArray());
+ var binaryData = document["vector"].AsBsonBinaryData;
+ return binaryData;
+ }
+
+ public readonly static IEnumerable